Skip to content

Commit 9028eb6

Browse files
committed
feat: add calculation vs CRUD service type templates
- Add separate templates for calculation services (scoring, analysis, transformation) - Add separate templates for CRUD services (entity management) - Auto-generate operations/index.ts barrel export - Auto-register routes in app.ts - Add db-extensions make target for pgvector support - Add generate:cli script alias - Fix notifyCreation critical flag (should be false) - Improve prompts with better defaults based on service type
1 parent 4263ef8 commit 9028eb6

14 files changed

Lines changed: 766 additions & 86 deletions

Makefile

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help setup up down restart logs dev test test-watch lint format typecheck build clean generate db-migrate db-studio db-push db-reset docker-clean all
1+
.PHONY: help setup up down restart logs dev test test-watch lint format typecheck build clean generate db-extensions db-migrate db-studio db-push db-reset docker-clean all
22

33
# Default target - show help
44
help:
@@ -32,6 +32,7 @@ help:
3232
@echo " make typecheck - Run TypeScript type checking"
3333
@echo ""
3434
@echo "🗄️ Database:"
35+
@echo " make db-extensions - Install PostgreSQL extensions (pgvector, etc.)"
3536
@echo " make db-migrate - Run Prisma migrations"
3637
@echo " make db-studio - Open Prisma Studio"
3738
@echo " make db-push - Push schema changes"
@@ -74,12 +75,18 @@ logs:
7475
@echo "📜 Showing Docker logs..."
7576
@npm run docker:logs
7677

78+
# Initialize PostgreSQL extensions (required for pgvector, etc.)
79+
db-extensions:
80+
@echo "🔌 Ensuring PostgreSQL extensions are installed..."
81+
@docker exec fastify_postgres psql -U postgres -d fastify_starter -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "vector";' 2>/dev/null || true
82+
7783
# Main entry point - like DriftOS!
7884
up: docker-up
7985
@echo "⏳ Waiting for PostgreSQL to be ready..."
8086
@sleep 8
81-
@echo "🗄️ Running database migrations..."
82-
@npm run db:migrate
87+
@$(MAKE) db-extensions
88+
@echo "🗄️ Pushing database schema..."
89+
@npm run db:push
8390
@echo "📊 Generating Grafana dashboards..."
8491
@npm run generate:dashboards || echo "⚠️ No orchestrators found yet"
8592
@echo ""

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"generate:operation": "plop operation",
3434
"generate:route": "plop route",
3535
"generate:dashboards": "tsx scripts/generate-dashboards.ts",
36+
"generate:cli": "node scripts/gen-service.mjs",
3637
"load-test": "tsx scripts/load-test.ts",
3738
"prepare": "husky install || true"
3839
},
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { {{pascalCase name}}Orchestrator } from './orchestrator.js';
2+
import type { {{pascalCase name}}Result, {{pascalCase name}}Policy, {{pascalCase name}}Input } from './types/index.js';
3+
import type { OrchestratorResult } from '@core/orchestration/index.js';
4+
5+
/**
6+
* {{pascalCase name}} Service (Calculation)
7+
*
8+
* Service facade implementing the singleton pattern.
9+
* Adapts external params to internal orchestrator.
10+
*
11+
* Generated by: npm run generate
12+
*/
13+
14+
/**
15+
* Default policy
16+
*/
17+
const DEFAULT_POLICY: {{pascalCase name}}Policy = {
18+
threshold: 0.75
19+
// TODO: Add policy defaults
20+
};
21+
22+
export class {{pascalCase name}}Service {
23+
private static instance: {{pascalCase name}}Service;
24+
private orchestrator: {{pascalCase name}}Orchestrator;
25+
26+
private constructor() {
27+
this.orchestrator = new {{pascalCase name}}Orchestrator();
28+
}
29+
30+
/**
31+
* Get singleton instance
32+
*/
33+
public static getInstance(): {{pascalCase name}}Service {
34+
if (!{{pascalCase name}}Service.instance) {
35+
{{pascalCase name}}Service.instance = new {{pascalCase name}}Service();
36+
}
37+
return {{pascalCase name}}Service.instance;
38+
}
39+
40+
/**
41+
* Calculate {{camelCase name}}
42+
*
43+
* @param id - Entity ID
44+
* @param data - Input data
45+
* @param policy - Optional policy override
46+
* @returns Calculation result
47+
*/
48+
async calculate(
49+
id: string,
50+
data: Record<string, unknown>,
51+
policy: Partial<{{pascalCase name}}Policy> = {}
52+
): Promise<OrchestratorResult<{{pascalCase name}}Result>> {
53+
const mergedPolicy: {{pascalCase name}}Policy = {
54+
...DEFAULT_POLICY,
55+
...policy
56+
};
57+
58+
return this.orchestrator.execute({
59+
id,
60+
data,
61+
policy: mergedPolicy
62+
});
63+
}
64+
65+
/**
66+
* Health check
67+
*/
68+
public async healthCheck(): Promise<{ status: string; service: string }> {
69+
return {
70+
status: 'healthy',
71+
service: '{{pascalCase name}}Service',
72+
};
73+
}
74+
}
75+
76+
// Export singleton instance
77+
export const {{camelCase name}}Service = {{pascalCase name}}Service.getInstance();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Create{{pascalCase name}}Orchestrator } from './orchestrator.js';
2+
import type { Create{{pascalCase name}}Input, {{pascalCase name}} } from './types/index.js';
3+
import type { OrchestratorResult } from '@core/orchestration/index.js';
4+
5+
/**
6+
* {{pascalCase name}} Service (CRUD)
7+
*
8+
* Service facade implementing the singleton pattern.
9+
* Public API for all {{camelCase name}} operations.
10+
*
11+
* Generated by: npm run generate
12+
*/
13+
export class {{pascalCase name}}Service {
14+
private static instance: {{pascalCase name}}Service;
15+
private createOrchestrator: Create{{pascalCase name}}Orchestrator;
16+
17+
private constructor() {
18+
this.createOrchestrator = new Create{{pascalCase name}}Orchestrator();
19+
}
20+
21+
/**
22+
* Get singleton instance
23+
*/
24+
public static getInstance(): {{pascalCase name}}Service {
25+
if (!{{pascalCase name}}Service.instance) {
26+
{{pascalCase name}}Service.instance = new {{pascalCase name}}Service();
27+
}
28+
return {{pascalCase name}}Service.instance;
29+
}
30+
31+
/**
32+
* Create a new {{camelCase name}}
33+
*/
34+
public async create{{pascalCase name}}(
35+
input: Create{{pascalCase name}}Input
36+
): Promise<OrchestratorResult<{{pascalCase name}}>> {
37+
return this.createOrchestrator.execute(input);
38+
}
39+
40+
/**
41+
* Health check
42+
*/
43+
public async healthCheck(): Promise<{ status: string; service: string }> {
44+
return {
45+
status: 'healthy',
46+
service: '{{pascalCase name}}Service',
47+
};
48+
}
49+
}
50+
51+
// Export singleton instance
52+
export const {{camelCase name}}Service = {{pascalCase name}}Service.getInstance();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { {{pascalCase name}}Context } from '../types/index.js';
2+
3+
/**
4+
* {{pascalCase operation}} Operation
5+
*
6+
* This is a pure function that takes context and returns context.
7+
* Implement your calculation logic here.
8+
*
9+
* Generated by: npm run generate
10+
*/
11+
export async function {{camelCase operation}}(
12+
ctx: {{pascalCase name}}Context
13+
): Promise<{{pascalCase name}}Context> {
14+
// Skip if previous errors
15+
if (ctx.errors.length > 0) {
16+
return ctx;
17+
}
18+
19+
// TODO: Implement your operation logic
20+
21+
// Store results in context
22+
ctx.results.{{camelCase operation}} = {
23+
completed: true,
24+
timestamp: new Date().toISOString(),
25+
};
26+
27+
return ctx;
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { {{pascalCase name}}PipelineContext } from '../types/index.js';
2+
3+
/**
4+
* {{pascalCase operation}} Operation
5+
*
6+
* This is a pure function that takes context and returns context.
7+
* Implement your business logic here.
8+
*
9+
* Generated by: npm run generate
10+
*/
11+
export async function {{camelCase operation}}(
12+
context: {{pascalCase name}}PipelineContext
13+
): Promise<{{pascalCase name}}PipelineContext> {
14+
// Skip if previous errors
15+
if (context.errors.length > 0) {
16+
return context;
17+
}
18+
19+
// TODO: Implement your operation logic
20+
21+
// Store results in context
22+
context.results.{{camelCase operation}} = {
23+
completed: true,
24+
timestamp: new Date().toISOString(),
25+
};
26+
27+
return context;
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* {{pascalCase name}} Operations
3+
* Barrel export
4+
*
5+
* Generated by: npm run generate
6+
*/
7+
8+
{{#each operations}}
9+
export { {{camelCase this}} } from './{{kebabCase this}}.js';
10+
{{/each}}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { BaseOrchestrator, DefaultPerformanceTracker } from '@core/orchestration/index.js';
2+
import type { PipelineStage } from '@core/orchestration/index.js';
3+
import type {
4+
{{pascalCase name}}Context,
5+
{{pascalCase name}}Result,
6+
{{pascalCase name}}Input
7+
} from './types/index.js';
8+
import * as ops from './operations/index.js';
9+
10+
/**
11+
* {{pascalCase name}} Orchestrator (Calculation)
12+
*
13+
* Internal orchestrator - called via {{pascalCase name}}Service facade.
14+
*
15+
* Generated by: npm run generate
16+
*/
17+
export class {{pascalCase name}}Orchestrator extends BaseOrchestrator<
18+
{{pascalCase name}}Context,
19+
{{pascalCase name}}Result,
20+
{{pascalCase name}}Input
21+
> {
22+
constructor() {
23+
super({
24+
name: '{{pascalCase name}}Orchestrator',
25+
timeout: 5000,
26+
enableMetrics: true,
27+
logErrors: true,
28+
});
29+
}
30+
31+
protected async initializeContext(input: {{pascalCase name}}Input): Promise<{{pascalCase name}}Context> {
32+
return {
33+
...input,
34+
policy: input.policy ?? { threshold: 0.75 },
35+
requestId: Math.random().toString(36).substr(2, 9),
36+
startTime: Date.now(),
37+
perfTracker: new DefaultPerformanceTracker(),
38+
results: {},
39+
errors: [],
40+
metadata: {
41+
orchestrator: this.getName(),
42+
},
43+
reasonCodes: []
44+
};
45+
}
46+
47+
protected getPipeline(): PipelineStage<{{pascalCase name}}Context>[] {
48+
return [
49+
{{#each operations}}
50+
{
51+
name: '{{kebabCase this}}',
52+
operation: ops.{{camelCase this}},
53+
critical: true,
54+
timeout: 2000,
55+
},
56+
{{/each}}
57+
];
58+
}
59+
60+
protected buildResult(ctx: {{pascalCase name}}Context): {{pascalCase name}}Result {
61+
return {
62+
score: ctx.score,
63+
reasonCodes: ctx.reasonCodes,
64+
metadata: ctx.metadata
65+
// TODO: Add result fields from context
66+
};
67+
}
68+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { BaseOrchestrator, DefaultPerformanceTracker } from '@core/orchestration/index.js';
2+
import type { PipelineStage } from '@core/orchestration/index.js';
3+
import type {
4+
{{pascalCase name}}PipelineContext,
5+
Create{{pascalCase name}}Input,
6+
{{pascalCase name}}
7+
} from './types/index.js';
8+
import * as ops from './operations/index.js';
9+
10+
/**
11+
* {{pascalCase name}} Orchestrator (CRUD)
12+
*
13+
* Implements the Golden Orchestrator pattern for {{pascalCase name}} operations.
14+
*
15+
* Generated by: npm run generate
16+
*/
17+
export class Create{{pascalCase name}}Orchestrator extends BaseOrchestrator<
18+
{{pascalCase name}}PipelineContext,
19+
{{pascalCase name}},
20+
Create{{pascalCase name}}Input
21+
> {
22+
constructor() {
23+
super({
24+
name: 'Create{{pascalCase name}}Orchestrator',
25+
timeout: 5000,
26+
enableMetrics: true,
27+
logErrors: true,
28+
});
29+
}
30+
31+
protected async initializeContext(
32+
input: Create{{pascalCase name}}Input
33+
): Promise<{{pascalCase name}}PipelineContext> {
34+
return {
35+
requestId: Math.random().toString(36).substr(2, 9),
36+
startTime: Date.now(),
37+
perfTracker: new DefaultPerformanceTracker(),
38+
input,
39+
results: {},
40+
errors: [],
41+
metadata: {
42+
orchestrator: this.getName(),
43+
inputType: 'Create{{pascalCase name}}Input',
44+
},
45+
};
46+
}
47+
48+
protected getPipeline(): PipelineStage<{{pascalCase name}}PipelineContext>[] {
49+
return [
50+
{{#each operations}}
51+
{
52+
name: '{{kebabCase this}}',
53+
operation: ops.{{camelCase this}},
54+
critical: true,
55+
timeout: 2000,
56+
},
57+
{{/each}}
58+
];
59+
}
60+
61+
protected buildResult(context: {{pascalCase name}}PipelineContext): {{pascalCase name}} {
62+
if (!context.{{camelCase name}}) {
63+
throw new Error('{{pascalCase name}} creation failed - no {{camelCase name}} in context');
64+
}
65+
return context.{{camelCase name}};
66+
}
67+
}

0 commit comments

Comments
 (0)