@mettlecast/domain-cli 0.2.60 → 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 +1 -36
- package/dist/builder/build-types.d.ts +1 -1
- package/dist/builder/load-module.d.ts +1 -1
- package/dist/cli.js +1 -1
- 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 +6 -22
- package/dist/commands/build-catalog.js +7 -18
- package/dist/commands/build.js +2 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.js +66 -37
- 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/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/validate.js +12 -90
- 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/scaffold-config.d.ts +6 -0
- package/dist/utils/scaffold-config.js +2 -0
- 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/create-project.test.ts +5 -5
- package/src/__tests__/commands/dev.test.ts +0 -1
- package/src/__tests__/mount-routes.test.ts +64 -23
- package/src/__tests__/package-freshness.test.ts +1 -21
- package/src/__tests__/smoke/scaffold.test.ts +13 -15
- package/src/__tests__/validate.test.ts +21 -103
- package/src/builder/build-registry.ts +7 -44
- package/src/builder/build-types.ts +1 -1
- package/src/cli.ts +1 -1
- 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 +13 -35
- package/src/commands/build.ts +3 -2
- package/src/commands/dev.ts +1 -1
- package/src/commands/doctor.ts +68 -37
- package/src/commands/explain.ts +13 -13
- package/src/commands/generate-openapi.ts +30 -52
- 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/validate.ts +11 -96
- 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/scaffold-config.ts +9 -0
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System-admin API pattern template.
|
|
3
3
|
* Produces: GET/POST with tenancy: 'system', no tenant context.
|
|
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 {
|
|
@@ -11,8 +15,7 @@ export function systemAdminTemplate(domain: string, id: string, tenancy: string)
|
|
|
11
15
|
const varName = camelCase(id);
|
|
12
16
|
|
|
13
17
|
return `import { z } from 'zod';
|
|
14
|
-
import {
|
|
15
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
18
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
16
19
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
17
20
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
18
21
|
|
|
@@ -30,7 +33,7 @@ const ${varName}Output = z.object({
|
|
|
30
33
|
data: z.record(z.unknown()).optional(),
|
|
31
34
|
}).default({ ok: true });
|
|
32
35
|
|
|
33
|
-
// ── System health
|
|
36
|
+
// ── System health output ─────────────────────────────────────
|
|
34
37
|
|
|
35
38
|
const SystemHealthOutput = z.object({
|
|
36
39
|
status: z.enum(['healthy', 'degraded', 'down']),
|
|
@@ -41,64 +44,68 @@ const SystemHealthOutput = z.object({
|
|
|
41
44
|
})),
|
|
42
45
|
}).default({ status: 'healthy', uptime: 0, domains: [] });
|
|
43
46
|
|
|
44
|
-
// ──
|
|
47
|
+
// ── Action definitions (API exposure) ────────────────────────
|
|
45
48
|
|
|
46
|
-
export const ${varName} =
|
|
49
|
+
export const ${varName} = defineAction({
|
|
47
50
|
id: '${id}',
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
backendAccess: 'platform',
|
|
52
|
+
exposure: {
|
|
53
|
+
type: 'api',
|
|
54
|
+
path: '/system/${domain}/${id}',
|
|
55
|
+
method: 'POST',
|
|
56
|
+
auth: 'service',
|
|
57
|
+
tenancy: '${tenancy}',
|
|
58
|
+
},
|
|
50
59
|
outboundAccess: 'internal',
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
handler: async (input, ctx) => {
|
|
57
|
-
await ctx.audit.log('${id}.system', 'system', { command: input.command });
|
|
60
|
+
idempotent: false,
|
|
61
|
+
input: ${varName}Input,
|
|
62
|
+
output: ${varName}Output,
|
|
63
|
+
handler: async (input, ctx) => {
|
|
64
|
+
await ctx.audit.log('${id}.system', 'system', { command: input.command });
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
},
|
|
66
|
+
switch (input.command) {
|
|
67
|
+
case 'status':
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
data: { status: 'healthy', domains: [] },
|
|
71
|
+
};
|
|
72
|
+
case 'metrics':
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
data: {
|
|
76
|
+
requestCount: 0,
|
|
77
|
+
errorCount: 0,
|
|
78
|
+
p99LatencyMs: 0,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
case 'health':
|
|
82
|
+
return { ok: true, data: { status: 'healthy' } };
|
|
83
|
+
default:
|
|
84
|
+
return { ok: false };
|
|
85
|
+
}
|
|
81
86
|
},
|
|
82
87
|
});
|
|
83
88
|
|
|
84
|
-
export const ${varName}Health =
|
|
89
|
+
export const ${varName}Health = defineAction({
|
|
85
90
|
id: '${id}-health',
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
backendAccess: 'platform',
|
|
92
|
+
exposure: {
|
|
93
|
+
type: 'api',
|
|
94
|
+
path: '/system/${domain}/${id}-health',
|
|
95
|
+
method: 'GET',
|
|
96
|
+
auth: 'service',
|
|
97
|
+
tenancy: '${tenancy}',
|
|
98
|
+
},
|
|
88
99
|
outboundAccess: 'internal',
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
domains: [],
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
},
|
|
100
|
+
idempotent: true,
|
|
101
|
+
input: z.object({}).default({}),
|
|
102
|
+
output: SystemHealthOutput,
|
|
103
|
+
handler: async (_input, _ctx) => {
|
|
104
|
+
return {
|
|
105
|
+
status: 'healthy',
|
|
106
|
+
uptime: process.uptime(),
|
|
107
|
+
domains: [],
|
|
108
|
+
};
|
|
102
109
|
},
|
|
103
110
|
});
|
|
104
111
|
`;
|
|
@@ -108,4 +115,4 @@ export const ${varName}Health = defineApi({
|
|
|
108
115
|
* Example body string used as the event body in the add-api fixture for the
|
|
109
116
|
* system-admin pattern. Mirrors the default shape of the input schema.
|
|
110
117
|
*/
|
|
111
|
-
export const systemAdminExampleBody = JSON.stringify({ command: 'status' });
|
|
118
|
+
export const systemAdminExampleBody = JSON.stringify({ command: 'status' });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Webhook-receiver-style API pattern template.
|
|
3
3
|
* Produces: POST with signature verification, provider enum.
|
|
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 {
|
|
@@ -9,10 +13,12 @@ function camelCase(s: string): string {
|
|
|
9
13
|
|
|
10
14
|
export function webhookReceiverStyleTemplate(domain: string, id: string, tenancy: string): string {
|
|
11
15
|
const varName = camelCase(id);
|
|
16
|
+
// Webhook receivers are typically public; pair `auth: 'none'` with a
|
|
17
|
+
// securityException.reason so the validator does not flag the route.
|
|
18
|
+
const securityException = `\n securityException: { reason: 'webhook receiver; signature verification handled by authorizer' },`;
|
|
12
19
|
|
|
13
20
|
return `import { z } from 'zod';
|
|
14
|
-
import {
|
|
15
|
-
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
21
|
+
import { defineAction, initOtel } from '@mettlecast/domain-runtime';
|
|
16
22
|
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
17
23
|
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
18
24
|
|
|
@@ -50,46 +56,48 @@ const ${varName}Output = z.object({
|
|
|
50
56
|
provider: 'stripe',
|
|
51
57
|
});
|
|
52
58
|
|
|
53
|
-
// ──
|
|
59
|
+
// ── Action definition (API exposure) ─────────────────────────
|
|
54
60
|
|
|
55
|
-
export const ${varName} =
|
|
61
|
+
export const ${varName} = defineAction({
|
|
56
62
|
id: '${id}',
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
backendAccess: 'domain',
|
|
64
|
+
exposure: {
|
|
65
|
+
type: 'api',
|
|
66
|
+
path: '/webhooks/${domain}/${id}',
|
|
67
|
+
method: 'POST',
|
|
68
|
+
auth: 'none',${securityException}
|
|
69
|
+
tenancy: '${tenancy}',
|
|
70
|
+
},
|
|
59
71
|
outboundAccess: 'internal',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
provider: input.provider,
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
},
|
|
72
|
+
idempotent: false,
|
|
73
|
+
input: ${varName}Input,
|
|
74
|
+
output: ${varName}Output,
|
|
75
|
+
handler: async (input, ctx) => {
|
|
76
|
+
// Signature verification belongs in a Lambda authorizer (x-webhook-signature header
|
|
77
|
+
// is not accessible here). Wire an HttpLambdaAuthorizer in DomainStack for production.
|
|
78
|
+
|
|
79
|
+
const eventId = crypto.randomUUID();
|
|
80
|
+
|
|
81
|
+
// ── Publish event for downstream processing ───────────
|
|
82
|
+
await ctx.publish('${domain}.webhook.received', {
|
|
83
|
+
eventId,
|
|
84
|
+
provider: input.provider,
|
|
85
|
+
eventType: input.eventType,
|
|
86
|
+
payload: input.payload,
|
|
87
|
+
tenantId: ctx.tenant.id,
|
|
88
|
+
}, 1);
|
|
89
|
+
|
|
90
|
+
await ctx.audit.log('${id}.webhook.received', ctx.tenant.id, {
|
|
91
|
+
provider: input.provider,
|
|
92
|
+
eventType: input.eventType,
|
|
93
|
+
eventId,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
received: true,
|
|
98
|
+
eventId,
|
|
99
|
+
provider: input.provider,
|
|
100
|
+
};
|
|
93
101
|
},
|
|
94
102
|
});
|
|
95
103
|
`;
|
|
@@ -103,4 +111,4 @@ export const webhookReceiverStyleExampleBody = JSON.stringify({
|
|
|
103
111
|
provider: 'stripe',
|
|
104
112
|
eventType: 'example.event',
|
|
105
113
|
payload: {},
|
|
106
|
-
});
|
|
114
|
+
});
|