@mettlecast/domain-cli 0.2.59 → 0.2.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/build-registry.d.ts +1 -1
- package/dist/builder/build-registry.js +129 -49
- package/dist/builder/build-types.d.ts +1 -1
- package/dist/builder/load-module.d.ts +1 -1
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +3 -3
- package/dist/commands/add-api.js +2 -2
- package/dist/commands/add-domain.js +4 -4
- package/dist/commands/add-fixture-factory.js +5 -6
- package/dist/commands/build-catalog.d.ts +8 -24
- package/dist/commands/build-catalog.js +12 -23
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +7 -6
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +110 -202
- package/dist/commands/explain.js +13 -13
- package/dist/commands/generate-openapi.d.ts +10 -1
- package/dist/commands/generate-openapi.js +19 -33
- package/dist/commands/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- package/dist/commands/show.d.ts +2 -3
- package/dist/commands/show.js +0 -2
- package/dist/commands/test.js +0 -1
- package/dist/commands/upgrade-backend.js +3 -3
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +144 -39
- package/dist/server/api-server.d.ts +1 -1
- package/dist/server/mount-routes.d.ts +11 -2
- package/dist/server/mount-routes.js +20 -8
- package/dist/templates/api-skeleton.d.ts +5 -0
- package/dist/templates/api-skeleton.js +28 -27
- package/dist/templates/claude-md.js +1 -1
- package/dist/templates/patterns/api/create-with-event.d.ts +4 -0
- package/dist/templates/patterns/api/create-with-event.js +38 -32
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +4 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +47 -41
- package/dist/templates/patterns/api/paginated-list.d.ts +4 -0
- package/dist/templates/patterns/api/paginated-list.js +30 -24
- package/dist/templates/patterns/api/simple-crud.d.ts +4 -0
- package/dist/templates/patterns/api/simple-crud.js +46 -35
- package/dist/templates/patterns/api/streaming-list.d.ts +4 -0
- package/dist/templates/patterns/api/streaming-list.js +46 -41
- package/dist/templates/patterns/api/system-admin.d.ts +4 -0
- package/dist/templates/patterns/api/system-admin.js +59 -52
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +4 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +43 -35
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/dist/utils/file-helpers.d.ts +0 -2
- package/dist/utils/file-helpers.js +2 -3
- package/dist/utils/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +8 -2
- package/dist/utils/scaffold-config.js +3 -1
- package/package.json +1 -1
- package/src/__tests__/build-registry.test.ts +43 -20
- package/src/__tests__/build-types.test.ts +4 -7
- package/src/__tests__/builder/walkDomainDir.test.ts +19 -21
- package/src/__tests__/commands/add-api.test.ts +12 -10
- package/src/__tests__/commands/add-domain.test.ts +8 -5
- package/src/__tests__/commands/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/create-project.test.ts +5 -5
- package/src/__tests__/commands/dev.test.ts +0 -1
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/mount-routes.test.ts +64 -23
- package/src/__tests__/package-freshness.test.ts +94 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/smoke/scaffold.test.ts +13 -15
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +570 -1
- package/src/builder/build-registry.ts +154 -59
- package/src/builder/build-types.ts +1 -1
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +4 -4
- package/src/commands/add-api.ts +2 -2
- package/src/commands/add-domain.ts +4 -4
- package/src/commands/add-fixture-factory.ts +5 -6
- package/src/commands/build-catalog.ts +18 -40
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +8 -7
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/dev.ts +1 -1
- package/src/commands/doctor.ts +120 -218
- package/src/commands/explain.ts +13 -13
- package/src/commands/generate-openapi.ts +30 -52
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/show.ts +2 -5
- package/src/commands/test.ts +0 -1
- package/src/commands/upgrade-backend.ts +3 -3
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +180 -40
- package/src/server/api-server.ts +1 -1
- package/src/server/mount-routes.ts +21 -10
- package/src/templates/api-skeleton.ts +29 -28
- package/src/templates/claude-md.ts +1 -1
- package/src/templates/patterns/api/create-with-event.ts +39 -33
- package/src/templates/patterns/api/idempotent-mutation.ts +48 -42
- package/src/templates/patterns/api/paginated-list.ts +31 -25
- package/src/templates/patterns/api/simple-crud.ts +47 -36
- package/src/templates/patterns/api/streaming-list.ts +47 -42
- package/src/templates/patterns/api/system-admin.ts +60 -53
- package/src/templates/patterns/api/webhook-receiver-style.ts +48 -40
- package/src/types.ts +128 -0
- package/src/utils/file-helpers.ts +2 -5
- package/src/utils/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +12 -3
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Template for generating API handler skeleton.
|
|
3
|
+
*
|
|
4
|
+
* Action-first contract: HTTP endpoints are declared with `defineAction`
|
|
5
|
+
* + `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
6
|
+
* removed in #4689 — the only public HTTP endpoint primitive is now an
|
|
7
|
+
* action whose exposure type is `'api'`.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
/**
|
|
@@ -21,45 +26,41 @@ function camelCase(s: string): string {
|
|
|
21
26
|
export function apiSkeletonTemplate(domainId: string, apiId: string, tenancy: string): string {
|
|
22
27
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
23
28
|
const outputType = `${camelCase(domainId)}${camelCase(apiId)}Output`;
|
|
29
|
+
// The `auth` posture must be set explicitly per the action-first contract:
|
|
30
|
+
// - 'required' for tenant-scoped or any authenticated endpoint.
|
|
31
|
+
// - 'none' for anonymous endpoints (then add `securityException`).
|
|
32
|
+
// - 'service' for service-to-service only.
|
|
33
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
24
34
|
return `import { z } from 'zod';
|
|
25
|
-
import {
|
|
26
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
35
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
27
36
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
28
37
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
29
38
|
|
|
30
39
|
// Initialise OpenTelemetry tracing for the process. Idempotent —
|
|
31
40
|
// safe to call at module scope. Sets up OTLP export (when
|
|
32
|
-
// OTEL_EXPORTER_OTLP_ENDPOINT is set) or console-export for
|
|
33
|
-
// local dev. Auto-instruments outbound HTTP calls via ky.
|
|
41
|
+
// OTEL_EXPORTER_OTLP_ENDPOINT is set) or console-export for local dev.
|
|
34
42
|
initOtel();
|
|
35
43
|
|
|
36
|
-
// Shared HTTP client with retry on 503/429. Replace this with
|
|
37
|
-
// your own client if you need custom headers or auth.
|
|
38
|
-
const http = createKyClient();
|
|
39
|
-
|
|
40
44
|
const outputSchema = z.object({ ok: z.boolean() }).default({ ok: true });
|
|
41
45
|
|
|
42
|
-
export const ${camelCase(apiId)} =
|
|
46
|
+
export const ${camelCase(apiId)} = defineAction({
|
|
43
47
|
id: '${apiId}',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
input: z.object({}).default({}),
|
|
52
|
-
output: outputSchema,
|
|
53
|
-
handler: async (_input, _ctx): Promise<Result<z.infer<typeof outputSchema>, AppError>> => {
|
|
54
|
-
// Replace this with your handler logic.
|
|
55
|
-
// Return ok(value) on success, err({ ... }) on failure.
|
|
56
|
-
return ok({ ok: true });
|
|
57
|
-
},
|
|
58
|
-
},
|
|
48
|
+
backendAccess: 'domain',
|
|
49
|
+
exposure: {
|
|
50
|
+
type: 'api',
|
|
51
|
+
path: '${tenantPath}/${domainId}/${apiId}',
|
|
52
|
+
method: 'POST',
|
|
53
|
+
auth: '${authPosture}',
|
|
54
|
+
tenancy: '${tenancy}',
|
|
59
55
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
outboundAccess: 'internal',
|
|
57
|
+
idempotent: false,
|
|
58
|
+
input: z.object({}).default({}),
|
|
59
|
+
output: outputSchema,
|
|
60
|
+
handler: async (_input, _ctx): Promise<Result<z.infer<typeof outputSchema>, AppError>> => {
|
|
61
|
+
// Replace this with your handler logic.
|
|
62
|
+
// Return ok(value) on success, err({ ... }) on failure.
|
|
63
|
+
return ok({ ok: true });
|
|
63
64
|
},
|
|
64
65
|
});
|
|
65
66
|
`;
|
|
@@ -101,4 +102,4 @@ export function apiFixtureSkeleton(
|
|
|
101
102
|
body: exampleBody,
|
|
102
103
|
},
|
|
103
104
|
}, null, 2) + '\n';
|
|
104
|
-
}
|
|
105
|
+
}
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export function claudeMdTemplate(id: string): string {
|
|
5
5
|
const titleName = id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
6
|
-
return `# ${titleName} Domain — Ownership\n\nYou are the domain owner for \`${id}\`.\n\n## Responsibilities\n\n- All source files under \`domains/${id}/\` are owned by you.\n-
|
|
6
|
+
return `# ${titleName} Domain — Ownership\n\nYou are the domain owner for \`${id}\`.\n\n## Responsibilities\n\n- All source files under \`domains/${id}/\` are owned by you.\n- Public HTTP endpoints must use \`defineAction({ exposure: { type: 'api', ... } })\` from \`@mettlecast/domain-runtime\`. The legacy \`defineApi\` factory is no longer scaffolded for public handlers.\n- Internal-only callable handlers must use \`defineAction({ exposure: { type: 'internal' } })\`.\n- Cross-domain calls must use \`ctx.actions.call()\`, never direct imports.\n- AWS SDK must never be imported directly — use \`ctx\` helpers.\n\n## Scaffold Rules\n\nSee the root \`CLAUDE.md\` for full scaffold rules, anti-patterns, and workflow commands.\n\n## Domain Config\n\nSee \`domains/${id}/domain.config.ts\` for the domain definition.\n`;
|
|
7
7
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create-with-event API pattern template.
|
|
3
3
|
* Produces: POST handler + publishes a domain event after mutation.
|
|
4
|
+
*
|
|
5
|
+
* Action-first: HTTP endpoints are declared with `defineAction` +
|
|
6
|
+
* `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
7
|
+
* removed in #4689.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
function camelCase(s: string): string {
|
|
@@ -10,10 +14,10 @@ function camelCase(s: string): string {
|
|
|
10
14
|
export function createWithEventTemplate(domain: string, id: string, tenancy: string): string {
|
|
11
15
|
const varName = camelCase(id);
|
|
12
16
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
17
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
13
18
|
|
|
14
19
|
return `import { z } from 'zod';
|
|
15
|
-
import {
|
|
16
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
20
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
17
21
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
18
22
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
19
23
|
|
|
@@ -34,43 +38,45 @@ const ${varName}Output = z.object({
|
|
|
34
38
|
status: 'created',
|
|
35
39
|
});
|
|
36
40
|
|
|
37
|
-
// ──
|
|
41
|
+
// ── Action definition (API exposure) ─────────────────────────
|
|
38
42
|
|
|
39
|
-
export const ${varName} =
|
|
43
|
+
export const ${varName} = defineAction({
|
|
40
44
|
id: '${id}',
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
backendAccess: 'domain',
|
|
46
|
+
exposure: {
|
|
47
|
+
type: 'api',
|
|
48
|
+
path: '${tenantPath}/${domain}/${id}',
|
|
49
|
+
method: 'POST',
|
|
50
|
+
auth: '${authPosture}',
|
|
51
|
+
tenancy: '${tenancy}',
|
|
52
|
+
},
|
|
43
53
|
outboundAccess: 'internal',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
handler: async (input, ctx) => {
|
|
50
|
-
const entityId = crypto.randomUUID();
|
|
54
|
+
idempotent: false,
|
|
55
|
+
input: ${varName}Input,
|
|
56
|
+
output: ${varName}Output,
|
|
57
|
+
handler: async (input, ctx) => {
|
|
58
|
+
const entityId = crypto.randomUUID();
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
// ── Persist entity ────────────────────────────────────
|
|
61
|
+
await ctx.store.put(entityId, { id: entityId, name: input.name });
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// ── Publish domain event ──────────────────────────────
|
|
64
|
+
await ctx.publish(
|
|
65
|
+
'${domain}.${id}.created',
|
|
66
|
+
{
|
|
67
|
+
id: entityId,
|
|
68
|
+
name: input.name,
|
|
69
|
+
tenantId: ctx.tenant.id,
|
|
70
|
+
},
|
|
71
|
+
1,
|
|
72
|
+
);
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
await ctx.audit.log('${id}.create', entityId, { tenantId: ctx.tenant.id });
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
},
|
|
76
|
+
return {
|
|
77
|
+
id: entityId,
|
|
78
|
+
status: 'created',
|
|
79
|
+
};
|
|
74
80
|
},
|
|
75
81
|
});
|
|
76
82
|
`;
|
|
@@ -80,4 +86,4 @@ export const ${varName} = defineApi({
|
|
|
80
86
|
* Example body string used as the event body in the add-api fixture for the
|
|
81
87
|
* create-with-event pattern. Mirrors the default shape of the input schema.
|
|
82
88
|
*/
|
|
83
|
-
export const createWithEventExampleBody = JSON.stringify({ name: 'Example' });
|
|
89
|
+
export const createWithEventExampleBody = JSON.stringify({ name: 'Example' });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Idempotent-mutation API pattern template.
|
|
3
3
|
* Produces: PUT with idempotency key middleware.
|
|
4
|
+
*
|
|
5
|
+
* Action-first: HTTP endpoints are declared with `defineAction` +
|
|
6
|
+
* `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
7
|
+
* removed in #4689.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
function camelCase(s: string): string {
|
|
@@ -10,10 +14,10 @@ function camelCase(s: string): string {
|
|
|
10
14
|
export function idempotentMutationTemplate(domain: string, id: string, tenancy: string): string {
|
|
11
15
|
const varName = camelCase(id);
|
|
12
16
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
17
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
13
18
|
|
|
14
19
|
return `import { z } from 'zod';
|
|
15
|
-
import {
|
|
16
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
20
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
17
21
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
18
22
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
19
23
|
|
|
@@ -39,53 +43,55 @@ const ${varName}Output = z.object({
|
|
|
39
43
|
idempotencyKey: '00000000-0000-0000-0000-000000000000',
|
|
40
44
|
});
|
|
41
45
|
|
|
42
|
-
// ──
|
|
46
|
+
// ── Action definition (API exposure) ─────────────────────────
|
|
43
47
|
|
|
44
|
-
export const ${varName} =
|
|
48
|
+
export const ${varName} = defineAction({
|
|
45
49
|
id: '${id}',
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
backendAccess: 'domain',
|
|
51
|
+
exposure: {
|
|
52
|
+
type: 'api',
|
|
53
|
+
path: '${tenantPath}/${domain}/${id}',
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
auth: '${authPosture}',
|
|
56
|
+
tenancy: '${tenancy}',
|
|
57
|
+
},
|
|
48
58
|
outboundAccess: 'internal',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// ── Check for duplicate (idempotency key = input.id) ──
|
|
56
|
-
const existing = await ctx.store.get('IDEMPOTENT#' + input.id);
|
|
59
|
+
idempotent: true,
|
|
60
|
+
input: ${varName}Input,
|
|
61
|
+
output: ${varName}Output,
|
|
62
|
+
handler: async (input, ctx) => {
|
|
63
|
+
// ── Check for duplicate (idempotency key = input.id) ──
|
|
64
|
+
const existing = await ctx.store.get('IDEMPOTENT#' + input.id);
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
if (existing) {
|
|
67
|
+
await ctx.audit.log('${id}.idempotent.duplicate', input.id);
|
|
68
|
+
return {
|
|
69
|
+
id: input.id,
|
|
70
|
+
status: 'already-processed',
|
|
71
|
+
idempotencyKey: input.id,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// ── Apply mutation ────────────────────────────────────
|
|
76
|
+
await ctx.store.put(
|
|
77
|
+
'${id.toUpperCase()}#' + input.id,
|
|
78
|
+
{ id: input.id, payload: input.payload, createdAt: new Date().toISOString() },
|
|
79
|
+
);
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// ── Record idempotency token with 24h TTL ─────────────
|
|
82
|
+
await ctx.store.put(
|
|
83
|
+
'IDEMPOTENT#' + input.id,
|
|
84
|
+
{ entityId: input.id },
|
|
85
|
+
{ ttl: Math.floor(Date.now() / 1000) + 86400 },
|
|
86
|
+
);
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
await ctx.audit.log('${id}.idempotent.applied', input.id, { tenantId: ctx.tenant.id });
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
},
|
|
90
|
+
return {
|
|
91
|
+
id: input.id,
|
|
92
|
+
status: 'applied',
|
|
93
|
+
idempotencyKey: input.id,
|
|
94
|
+
};
|
|
89
95
|
},
|
|
90
96
|
});
|
|
91
97
|
`;
|
|
@@ -98,4 +104,4 @@ export const ${varName} = defineApi({
|
|
|
98
104
|
export const idempotentMutationExampleBody = JSON.stringify({
|
|
99
105
|
id: '00000000-0000-0000-0000-000000000000',
|
|
100
106
|
payload: {},
|
|
101
|
-
});
|
|
107
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Paginated-list API pattern template.
|
|
3
3
|
* Produces: GET with cursor/limit, pagination contract, EMF metrics.
|
|
4
|
+
*
|
|
5
|
+
* Action-first: HTTP endpoints are declared with `defineAction` +
|
|
6
|
+
* `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
7
|
+
* removed in #4689.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
function camelCase(s: string): string {
|
|
@@ -10,10 +14,10 @@ function camelCase(s: string): string {
|
|
|
10
14
|
export function paginatedListTemplate(domain: string, id: string, tenancy: string): string {
|
|
11
15
|
const varName = camelCase(id);
|
|
12
16
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
17
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
13
18
|
|
|
14
19
|
return `import { z } from 'zod';
|
|
15
|
-
import {
|
|
16
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
20
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
17
21
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
18
22
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
19
23
|
|
|
@@ -38,34 +42,36 @@ const PageOutput = z.object({
|
|
|
38
42
|
total: z.number().int().optional(),
|
|
39
43
|
}).default({ items: [], nextCursor: undefined, total: undefined });
|
|
40
44
|
|
|
41
|
-
// ──
|
|
45
|
+
// ── Action definition (API exposure) ─────────────────────────
|
|
42
46
|
|
|
43
|
-
export const ${varName} =
|
|
47
|
+
export const ${varName} = defineAction({
|
|
44
48
|
id: '${id}',
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
backendAccess: 'domain',
|
|
50
|
+
exposure: {
|
|
51
|
+
type: 'api',
|
|
52
|
+
path: '${tenantPath}/${domain}/${id}',
|
|
53
|
+
method: 'GET',
|
|
54
|
+
auth: '${authPosture}',
|
|
55
|
+
tenancy: '${tenancy}',
|
|
56
|
+
},
|
|
47
57
|
outboundAccess: 'internal',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
handler: async (input, ctx) => {
|
|
54
|
-
const { cursor, limit, filter } = input;
|
|
58
|
+
idempotent: true,
|
|
59
|
+
input: PageInput,
|
|
60
|
+
output: PageOutput,
|
|
61
|
+
handler: async (input, ctx) => {
|
|
62
|
+
const { cursor, limit, filter } = input;
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
await ctx.audit.log('${id}.list', ctx.tenant.id, { cursor, limit });
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
// ── paginated data fetch ──────────────────────────────
|
|
67
|
+
const items: Array<{ id: string }> = [];
|
|
68
|
+
const nextCursor = items.length === limit ? btoa(items[items.length - 1].id) : undefined;
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
},
|
|
70
|
+
return {
|
|
71
|
+
items,
|
|
72
|
+
nextCursor,
|
|
73
|
+
total: undefined,
|
|
74
|
+
};
|
|
69
75
|
},
|
|
70
76
|
});
|
|
71
77
|
`;
|
|
@@ -75,4 +81,4 @@ export const ${varName} = defineApi({
|
|
|
75
81
|
* Example body string used as the event body in the add-api fixture for the
|
|
76
82
|
* paginated-list pattern. Mirrors the default shape of the input schema.
|
|
77
83
|
*/
|
|
78
|
-
export const paginatedListExampleBody = '{}';
|
|
84
|
+
export const paginatedListExampleBody = '{}';
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Simple CRUD API pattern template.
|
|
3
3
|
* Produces: GET / POST / PUT / DELETE with Zod schemas and audit logging.
|
|
4
|
+
*
|
|
5
|
+
* Action-first: HTTP endpoints are declared with `defineAction` +
|
|
6
|
+
* `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
7
|
+
* removed in #4689.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
function camelCase(s: string): string {
|
|
@@ -10,10 +14,10 @@ function camelCase(s: string): string {
|
|
|
10
14
|
export function simpleCrudTemplate(domain: string, id: string, tenancy: string): string {
|
|
11
15
|
const varName = camelCase(id);
|
|
12
16
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
17
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
13
18
|
|
|
14
19
|
return `import { z } from 'zod';
|
|
15
|
-
import {
|
|
16
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
20
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
17
21
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
18
22
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
19
23
|
|
|
@@ -40,28 +44,30 @@ const ${varName}ListOutput = z.object({
|
|
|
40
44
|
nextCursor: z.string().optional(),
|
|
41
45
|
}).default({ items: [], nextCursor: undefined });
|
|
42
46
|
|
|
43
|
-
// ──
|
|
47
|
+
// ── Action definition (API exposure) ─────────────────────────
|
|
44
48
|
|
|
45
|
-
export const ${varName} =
|
|
49
|
+
export const ${varName} = defineAction({
|
|
46
50
|
id: '${id}',
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
backendAccess: 'domain',
|
|
52
|
+
exposure: {
|
|
53
|
+
type: 'api',
|
|
54
|
+
path: '${tenantPath}/${domain}/${id}',
|
|
55
|
+
method: 'POST',
|
|
56
|
+
auth: '${authPosture}',
|
|
57
|
+
tenancy: '${tenancy}',
|
|
58
|
+
},
|
|
49
59
|
outboundAccess: 'internal',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
updatedAt: new Date().toISOString(),
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
},
|
|
60
|
+
idempotent: false,
|
|
61
|
+
input: ${varName}Input,
|
|
62
|
+
output: ${varName}Output,
|
|
63
|
+
handler: async (input, ctx) => {
|
|
64
|
+
// Create / Read / Update / Delete based on method
|
|
65
|
+
await ctx.audit.log('${id}.v1', ctx.tenant.id, { input });
|
|
66
|
+
return {
|
|
67
|
+
id: input.id ?? crypto.randomUUID(),
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
updatedAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
65
71
|
},
|
|
66
72
|
});
|
|
67
73
|
`;
|
|
@@ -70,9 +76,12 @@ export const ${varName} = defineApi({
|
|
|
70
76
|
export function simpleCrudListTemplate(domain: string, id: string, tenancy: string): string {
|
|
71
77
|
const varName = camelCase(id);
|
|
72
78
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
79
|
+
const authPosture = tenancy === 'system' ? 'service' : 'required';
|
|
73
80
|
|
|
74
81
|
return `import { z } from 'zod';
|
|
75
|
-
import {
|
|
82
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
83
|
+
|
|
84
|
+
initOtel();
|
|
76
85
|
|
|
77
86
|
const ${varName}ListInput = z.object({
|
|
78
87
|
cursor: z.string().optional(),
|
|
@@ -94,21 +103,23 @@ const ${varName}ListOutput = z.object({
|
|
|
94
103
|
nextCursor: z.string().optional(),
|
|
95
104
|
}).default({ items: [], nextCursor: undefined });
|
|
96
105
|
|
|
97
|
-
export const ${varName}List =
|
|
106
|
+
export const ${varName}List = defineAction({
|
|
98
107
|
id: '${id}-list',
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
backendAccess: 'domain',
|
|
109
|
+
exposure: {
|
|
110
|
+
type: 'api',
|
|
111
|
+
path: '${tenantPath}/${domain}/${id}-list',
|
|
112
|
+
method: 'GET',
|
|
113
|
+
auth: '${authPosture}',
|
|
114
|
+
tenancy: '${tenancy}',
|
|
115
|
+
},
|
|
101
116
|
outboundAccess: 'internal',
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await ctx.audit.log('${id}-list.v1', ctx.tenant.id);
|
|
109
|
-
return { items: [], nextCursor: undefined };
|
|
110
|
-
},
|
|
111
|
-
},
|
|
117
|
+
idempotent: true,
|
|
118
|
+
input: ${varName}ListInput,
|
|
119
|
+
output: ${varName}ListOutput,
|
|
120
|
+
handler: async (input, ctx) => {
|
|
121
|
+
await ctx.audit.log('${id}-list.v1', ctx.tenant.id);
|
|
122
|
+
return { items: [], nextCursor: undefined };
|
|
112
123
|
},
|
|
113
124
|
});
|
|
114
125
|
`;
|
|
@@ -118,4 +129,4 @@ export const ${varName}List = defineApi({
|
|
|
118
129
|
* Example body string used as the event body in the add-api fixture for the
|
|
119
130
|
* simple-crud pattern. Mirrors the default shape of the input schema.
|
|
120
131
|
*/
|
|
121
|
-
export const simpleCrudExampleBody = '{}';
|
|
132
|
+
export const simpleCrudExampleBody = '{}';
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* - API Gateway HTTP API with 2.0 payload format (not REST API)
|
|
20
20
|
* - Lambda runtime v20+
|
|
21
21
|
*
|
|
22
|
+
* Action-first: HTTP endpoints are declared with `defineAction` +
|
|
23
|
+
* `exposure: { type: 'api', ... }`. The legacy `defineApi` factory was
|
|
24
|
+
* removed in #4689.
|
|
25
|
+
*
|
|
22
26
|
* @param domain - Domain ID in kebab-case
|
|
23
27
|
* @param apiId - API ID in kebab-case
|
|
24
28
|
* @param itemName - Singular item name (e.g. 'ticket', 'member')
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
*/
|
|
27
31
|
export function streamingListPattern(domain: string, apiId: string, itemName: string) {
|
|
28
32
|
return `import { z } from 'zod';
|
|
29
|
-
import {
|
|
33
|
+
import { defineAction } from '@mettlecast/domain-runtime';
|
|
30
34
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
31
35
|
|
|
32
36
|
const ${itemName}Schema = z.object({ id: z.string() });
|
|
@@ -39,54 +43,55 @@ const outputSchema = z.object({
|
|
|
39
43
|
nextCursor: z.string().optional(),
|
|
40
44
|
}).default({ items: [] });
|
|
41
45
|
|
|
42
|
-
export const ${apiId.replace(/-/g, '_')} =
|
|
46
|
+
export const ${apiId.replace(/-/g, '_')} = defineAction({
|
|
43
47
|
id: '${apiId}',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
backendAccess: 'domain',
|
|
49
|
+
exposure: {
|
|
50
|
+
type: 'api',
|
|
51
|
+
path: '/v1/tenants/{tenantId}/${domain}/${apiId}',
|
|
52
|
+
method: 'GET',
|
|
53
|
+
auth: 'required',
|
|
54
|
+
tenancy: 'required',
|
|
55
|
+
},
|
|
47
56
|
outboundAccess: 'internal',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// {"id":"2"}\\n
|
|
64
|
-
// {"id":"3"}\\n
|
|
57
|
+
idempotent: true,
|
|
58
|
+
input: inputSchema,
|
|
59
|
+
output: outputSchema,
|
|
60
|
+
handler: async (input, ctx): Promise<Result<z.infer<typeof outputSchema>, AppError>> => {
|
|
61
|
+
// Streaming response: each item is yielded as a JSON line
|
|
62
|
+
// before the full query completes. The runtime handler
|
|
63
|
+
// wrapper converts this generator into a chunked response.
|
|
64
|
+
//
|
|
65
|
+
// GET /v1/${domain}/${apiId}?limit=100
|
|
66
|
+
// Transfer-Encoding: chunked
|
|
67
|
+
// Content-Type: application/x-ndjson
|
|
68
|
+
//
|
|
69
|
+
// {"id":"1"}\\n
|
|
70
|
+
// {"id":"2"}\\n
|
|
71
|
+
// {"id":"3"}\\n
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
let nextCursor: string | undefined;
|
|
74
|
+
const items: unknown[] = [];
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
// Replace this query with your actual data source.
|
|
77
|
+
const rows = await ctx.db.query(
|
|
78
|
+
'${domain}',
|
|
79
|
+
'SELECT id FROM ${domain}s LIMIT $1 OFFSET $2',
|
|
80
|
+
[input.limit + 1, input.cursor ? parseInt(input.cursor, 10) : 0],
|
|
81
|
+
);
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
for (let i = 0; i < Math.min(rows.length, input.limit); i++) {
|
|
84
|
+
items.push(rows[i]);
|
|
85
|
+
}
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
if (rows.length > input.limit) {
|
|
88
|
+
nextCursor = String(
|
|
89
|
+
(input.cursor ? parseInt(input.cursor, 10) : 0) + input.limit,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
},
|
|
93
|
+
return { ok: true, value: { items, nextCursor } };
|
|
89
94
|
},
|
|
90
95
|
});
|
|
91
96
|
`;
|
|
92
|
-
}
|
|
97
|
+
}
|