@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,22 +1,123 @@
|
|
|
1
1
|
import { relative, resolve } from 'node:path';
|
|
2
2
|
import type {
|
|
3
3
|
DomainRegistry,
|
|
4
|
-
|
|
5
|
-
ApiVersionSnapshot,
|
|
4
|
+
ActionRegistryEntry,
|
|
6
5
|
SchemaSnapshot,
|
|
7
6
|
WebhookRegistryEntry,
|
|
8
7
|
SubscriberRegistryEntry,
|
|
9
8
|
ScheduleRegistryEntry,
|
|
10
9
|
JobRegistryEntry,
|
|
11
|
-
ActionRegistryEntry,
|
|
12
10
|
IntegrationRegistryEntry,
|
|
13
11
|
EventRegistryEntry,
|
|
14
12
|
DomainRegistryEntry,
|
|
15
13
|
SerialDeploymentConfig,
|
|
16
|
-
} from '
|
|
14
|
+
} from '../types.js';
|
|
17
15
|
import { walkDomainDir } from '../utils/file-helpers.js';
|
|
18
16
|
import { loadModuleExports, type RawPrimitiveExport } from './load-module.js';
|
|
19
17
|
|
|
18
|
+
// Issue #4689: the legacy `defineApi` primitive was removed from
|
|
19
|
+
// `@mettlecast/domain-runtime`. The only HTTP endpoint surface in
|
|
20
|
+
// the new registry is `actions[]` whose `exposure.type === 'api'`.
|
|
21
|
+
|
|
22
|
+
type ActionBackendAccess = 'private' | 'domain' | 'platform';
|
|
23
|
+
|
|
24
|
+
type ActionExposure =
|
|
25
|
+
| { type: 'internal' }
|
|
26
|
+
| {
|
|
27
|
+
type: 'api';
|
|
28
|
+
path: string;
|
|
29
|
+
method: string;
|
|
30
|
+
auth: 'required' | 'none' | 'service';
|
|
31
|
+
tenancy: 'required' | 'none' | 'system';
|
|
32
|
+
roles?: string[];
|
|
33
|
+
securityException?: { reason: string };
|
|
34
|
+
authDeclared?: boolean;
|
|
35
|
+
tenancyDeclared?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Coerce a raw value into a valid backendAccess scope, defaulting to
|
|
40
|
+
* 'private' when the value is missing or unrecognized. Used by the
|
|
41
|
+
* builder when reading the new-style `backendAccess` field directly.
|
|
42
|
+
*/
|
|
43
|
+
function toBackendAccess(raw: unknown): ActionBackendAccess {
|
|
44
|
+
return raw === 'domain' || raw === 'platform' ? raw : 'private';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Map a legacy `visibility` scope to a `backendAccess` scope for the
|
|
49
|
+
* action-first migration. The legacy `workspace` scope (which previously
|
|
50
|
+
* permitted unauthenticated Function URL exposure) collapses to `domain`
|
|
51
|
+
* so that all cross-domain callers must go through `ctx.actions`.
|
|
52
|
+
*/
|
|
53
|
+
function visibilityToBackendAccess(visibility: unknown): ActionBackendAccess {
|
|
54
|
+
if (visibility === 'workspace') return 'domain';
|
|
55
|
+
if (visibility === 'domain') return 'domain';
|
|
56
|
+
return 'private';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Best-effort reverse mapping from `backendAccess` to the legacy
|
|
61
|
+
* `visibility` field. Used to keep the deprecated field populated so
|
|
62
|
+
* existing CDK constructs that read it continue to behave the same way.
|
|
63
|
+
*
|
|
64
|
+
* - private -> private
|
|
65
|
+
* - domain -> domain
|
|
66
|
+
* - platform -> workspace (closest legacy equivalent for platform-level)
|
|
67
|
+
*/
|
|
68
|
+
function backendAccessToVisibility(backendAccess: ActionBackendAccess): 'private' | 'domain' | 'workspace' {
|
|
69
|
+
if (backendAccess === 'platform') return 'workspace';
|
|
70
|
+
return backendAccess;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Type guard + sanitizer for an action's `exposure` field. Returns a
|
|
75
|
+
* well-typed `ActionExposure` (extended with `authDeclared` /
|
|
76
|
+
* `tenancyDeclared` tracking flags) or the provided fallback when the
|
|
77
|
+
* raw value does not match a supported exposure shape.
|
|
78
|
+
*
|
|
79
|
+
* The `authDeclared` / `tenancyDeclared` flags record whether the source
|
|
80
|
+
* code explicitly declared each field, or whether the builder fell back
|
|
81
|
+
* to the safe default. They are validation-only and consumed by the
|
|
82
|
+
* domain CLI's validate command (Wave 6 Task 6.1) to enforce the
|
|
83
|
+
* action-first security model (#4619).
|
|
84
|
+
*/
|
|
85
|
+
function toExposure(raw: unknown, fallback: ActionExposure): ActionExposure {
|
|
86
|
+
if (!raw || typeof raw !== 'object') return fallback;
|
|
87
|
+
const candidate = raw as { type?: unknown };
|
|
88
|
+
if (candidate.type === 'internal') return { type: 'internal' };
|
|
89
|
+
if (candidate.type !== 'api') return fallback;
|
|
90
|
+
// Best-effort validation of api exposure fields; any missing required
|
|
91
|
+
// string field falls back to the supplied default exposure.
|
|
92
|
+
const api = raw as { path?: unknown; method?: unknown; auth?: unknown; tenancy?: unknown; roles?: unknown; securityException?: unknown };
|
|
93
|
+
if (typeof api.path !== 'string' || typeof api.method !== 'string') return fallback;
|
|
94
|
+
const authRaw = api.auth;
|
|
95
|
+
const tenancyRaw = api.tenancy;
|
|
96
|
+
const auth: 'required' | 'none' | 'service' =
|
|
97
|
+
authRaw === 'required' || authRaw === 'none' || authRaw === 'service' ? authRaw : 'required';
|
|
98
|
+
const tenancy: 'required' | 'none' | 'system' =
|
|
99
|
+
tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system' ? tenancyRaw : 'required';
|
|
100
|
+
const out: ActionExposure & { type: 'api'; authDeclared?: boolean; tenancyDeclared?: boolean } = {
|
|
101
|
+
type: 'api',
|
|
102
|
+
path: api.path,
|
|
103
|
+
method: api.method,
|
|
104
|
+
auth,
|
|
105
|
+
tenancy,
|
|
106
|
+
authDeclared: authRaw === 'required' || authRaw === 'none' || authRaw === 'service',
|
|
107
|
+
tenancyDeclared: tenancyRaw === 'required' || tenancyRaw === 'none' || tenancyRaw === 'system',
|
|
108
|
+
};
|
|
109
|
+
if (Array.isArray(api.roles)) {
|
|
110
|
+
out.roles = api.roles.filter((r): r is string => typeof r === 'string');
|
|
111
|
+
}
|
|
112
|
+
if (api.securityException && typeof api.securityException === 'object') {
|
|
113
|
+
const reason = (api.securityException as { reason?: unknown }).reason;
|
|
114
|
+
if (typeof reason === 'string') {
|
|
115
|
+
out.securityException = { reason };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
20
121
|
/** Returns true if a value looks like a JSON Schema object (has a 'type' or '$schema' property). */
|
|
21
122
|
function isJsonSchema(v: unknown): boolean {
|
|
22
123
|
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
@@ -79,7 +180,7 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
79
180
|
const domainRaw = domainExports.find(e => e['_kind'] === 'domain');
|
|
80
181
|
if (!domainRaw) {
|
|
81
182
|
// Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
|
|
82
|
-
for (const w of warnings) process.stderr.write(`[
|
|
183
|
+
for (const w of warnings) process.stderr.write(`[mc-domain-module validate] ${w}\n`);
|
|
83
184
|
throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
|
|
84
185
|
}
|
|
85
186
|
|
|
@@ -91,9 +192,8 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
91
192
|
defaultDeployment: deployment(domainRaw),
|
|
92
193
|
};
|
|
93
194
|
|
|
94
|
-
|
|
195
|
+
const [webhookExports, subscriberExports, actionExports,
|
|
95
196
|
scheduleExports, jobExports, integrationExports, eventExports] = await Promise.all([
|
|
96
|
-
Promise.all(paths.apis.map(load)),
|
|
97
197
|
Promise.all(paths.webhooks.map(load)),
|
|
98
198
|
Promise.all(paths.subscribers.map(load)),
|
|
99
199
|
Promise.all(paths.actions.map(load)),
|
|
@@ -103,43 +203,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
103
203
|
paths.publishes ? load(paths.publishes) : Promise.resolve([]),
|
|
104
204
|
]);
|
|
105
205
|
|
|
106
|
-
const VALID_API_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
107
|
-
|
|
108
|
-
const apis: ApiRegistryEntry[] = paths.apis.flatMap((filePath, i) =>
|
|
109
|
-
(apiExports[i] ?? [])
|
|
110
|
-
.filter(e => e['_kind'] === 'api')
|
|
111
|
-
.map(e => {
|
|
112
|
-
const rawMethod = typeof e['method'] === 'string' ? e['method'].toUpperCase() : '';
|
|
113
|
-
if (!rawMethod || !VALID_API_METHODS.has(rawMethod)) {
|
|
114
|
-
warnings.push(`${relPath(filePath)}: defineApi "${e['id']}" has invalid or missing method "${rawMethod || '(none)'}". Use one of: ${[...VALID_API_METHODS].join(', ')}.`);
|
|
115
|
-
}
|
|
116
|
-
const rawVersions = e['versions'] as Record<string, { input?: unknown; output?: unknown }> | undefined;
|
|
117
|
-
const versionSnapshots: ApiVersionSnapshot[] = rawVersions
|
|
118
|
-
? Object.entries(rawVersions).map(([ver, v]) => ({
|
|
119
|
-
version: ver,
|
|
120
|
-
requestSchema: isJsonSchema(v?.input) ? (v.input as SchemaSnapshot) : undefined,
|
|
121
|
-
responseSchema: isJsonSchema(v?.output) ? (v.output as SchemaSnapshot) : undefined,
|
|
122
|
-
}))
|
|
123
|
-
: [];
|
|
124
|
-
const latestVersion = versionSnapshots[versionSnapshots.length - 1];
|
|
125
|
-
return {
|
|
126
|
-
id: String(e['id']),
|
|
127
|
-
kind: 'api' as const,
|
|
128
|
-
handlerFile: relPath(filePath),
|
|
129
|
-
path: String(e['path']),
|
|
130
|
-
method: VALID_API_METHODS.has(rawMethod) ? rawMethod : 'GET',
|
|
131
|
-
authType: ((e['auth'] as { type?: string } | undefined)?.type as 'jwt' | 'api-key' | 'none' | undefined) ?? 'jwt',
|
|
132
|
-
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
133
|
-
deployment: deployment(e),
|
|
134
|
-
outboundAccess: outboundAccess(e),
|
|
135
|
-
requestSchema: latestVersion?.requestSchema,
|
|
136
|
-
responseSchema: latestVersion?.responseSchema,
|
|
137
|
-
versions: versionSnapshots.length > 0 ? versionSnapshots : undefined,
|
|
138
|
-
examples: (e['examples'] as { request?: Record<string, unknown>; response?: Record<string, unknown> } | undefined),
|
|
139
|
-
};
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
|
|
143
206
|
const webhooks: WebhookRegistryEntry[] = paths.webhooks.flatMap((filePath, i) =>
|
|
144
207
|
(webhookExports[i] ?? [])
|
|
145
208
|
.filter(e => e['_kind'] === 'webhook')
|
|
@@ -199,22 +262,55 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
199
262
|
}))
|
|
200
263
|
);
|
|
201
264
|
|
|
202
|
-
const actions
|
|
265
|
+
const actions = paths.actions.flatMap((filePath, i) =>
|
|
203
266
|
(actionExports[i] ?? [])
|
|
204
267
|
.filter(e => e['_kind'] === 'action')
|
|
205
|
-
.map(e =>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
268
|
+
.map(e => {
|
|
269
|
+
// Resolve backendAccess. New-style actions carry `backendAccess`
|
|
270
|
+
// directly. Legacy actions only carry `visibility`; in that case
|
|
271
|
+
// we collapse workspace -> domain per the migration spec, and we
|
|
272
|
+
// also remember the legacy flag so we can default exposure to
|
|
273
|
+
// `{ type: 'internal' }` for actions that have not opted in yet.
|
|
274
|
+
const hasBackendAccess = 'backendAccess' in e;
|
|
275
|
+
const rawBackendAccess = e['backendAccess'];
|
|
276
|
+
const rawVisibility = e['visibility'];
|
|
277
|
+
const backendAccess: ActionBackendAccess = hasBackendAccess
|
|
278
|
+
? toBackendAccess(rawBackendAccess)
|
|
279
|
+
: visibilityToBackendAccess(rawVisibility);
|
|
280
|
+
const legacyVisibility = backendAccessToVisibility(backendAccess);
|
|
281
|
+
|
|
282
|
+
// Resolve exposure. New-style actions must declare their exposure;
|
|
283
|
+
// legacy actions that did not opt in default to `{ type: 'internal' }`
|
|
284
|
+
// so existing internal-only behavior is preserved during migration.
|
|
285
|
+
const rawExposure = e['exposure'];
|
|
286
|
+
const exposure: ActionExposure = toExposure(rawExposure, { type: 'internal' });
|
|
287
|
+
// Record whether the source explicitly declared the `exposure` field.
|
|
288
|
+
// Validation-only; consumed by the domain CLI's validate command
|
|
289
|
+
// (Wave 6 Task 6.1) to enforce `ACTION_EXPOSURE_REQUIRED`.
|
|
290
|
+
const exposureDeclared = rawExposure !== undefined && rawExposure !== null
|
|
291
|
+
&& typeof rawExposure === 'object';
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
id: String(e['id']),
|
|
295
|
+
kind: 'action' as const,
|
|
296
|
+
handlerFile: relPath(filePath),
|
|
297
|
+
backendAccess,
|
|
298
|
+
exposure,
|
|
299
|
+
exposureDeclared,
|
|
300
|
+
// Keep the legacy field populated so CDK constructs that still
|
|
301
|
+
// read `visibility` (e.g. action-construct.ts) keep working
|
|
302
|
+
// through the migration window. New constructs should read
|
|
303
|
+
// `backendAccess` and `exposure` instead.
|
|
304
|
+
visibility: legacyVisibility,
|
|
305
|
+
idempotent: Boolean(e['idempotent'] ?? false),
|
|
306
|
+
description: typeof e['description'] === 'string' ? e['description'] : undefined,
|
|
307
|
+
deployment: deployment(e),
|
|
308
|
+
outboundAccess: outboundAccess(e),
|
|
309
|
+
inputSchema: isJsonSchema(e['input']) ? (e['input'] as SchemaSnapshot) : undefined,
|
|
310
|
+
outputSchema: isJsonSchema(e['output']) ? (e['output'] as SchemaSnapshot) : undefined,
|
|
311
|
+
};
|
|
312
|
+
})
|
|
313
|
+
) as ActionRegistryEntry[];
|
|
218
314
|
|
|
219
315
|
const integrations: IntegrationRegistryEntry[] = paths.integrations.flatMap((_filePath, i) =>
|
|
220
316
|
(integrationExports[i] ?? [])
|
|
@@ -242,7 +338,6 @@ export async function buildRegistry(domainRoot: string): Promise<BuildResult> {
|
|
|
242
338
|
schemaVersion: '1',
|
|
243
339
|
domainRoot,
|
|
244
340
|
domain,
|
|
245
|
-
apis,
|
|
246
341
|
webhooks,
|
|
247
342
|
subscribers,
|
|
248
343
|
schedules,
|
|
@@ -113,9 +113,9 @@ process.stdout.write(JSON.stringify(results));
|
|
|
113
113
|
export async function loadModuleExports(absoluteFilePath: string): Promise<RawPrimitiveExport[]> {
|
|
114
114
|
// Use a subdir of the project root rather than OS tmpdir so that ESM import
|
|
115
115
|
// resolution can walk up and find node_modules packages like zod-to-json-schema.
|
|
116
|
-
const
|
|
117
|
-
await mkdir(
|
|
118
|
-
const tempPath = join(
|
|
116
|
+
const mcTmpDir = join(process.cwd(), '.mc', 'tmp');
|
|
117
|
+
await mkdir(mcTmpDir, { recursive: true }).catch(() => undefined);
|
|
118
|
+
const tempPath = join(mcTmpDir, `mc-load-${randomBytes(8).toString('hex')}.mts`);
|
|
119
119
|
await writeFile(tempPath, makeEvalScript(absoluteFilePath), 'utf8');
|
|
120
120
|
|
|
121
121
|
try {
|
package/src/cli.ts
CHANGED
|
@@ -73,7 +73,7 @@ program
|
|
|
73
73
|
|
|
74
74
|
program
|
|
75
75
|
.command('dev <domain>')
|
|
76
|
-
.description('Start a local HTTP server simulating API Gateway for all
|
|
76
|
+
.description('Start a local HTTP server simulating API Gateway for all API-exposed defineAction handlers')
|
|
77
77
|
.option('--port <n>', 'Port to listen on', '3000')
|
|
78
78
|
.action(async (domain: string, opts: { port?: string }) => {
|
|
79
79
|
await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
|
|
@@ -82,9 +82,9 @@ program
|
|
|
82
82
|
program
|
|
83
83
|
.command('build-catalog')
|
|
84
84
|
.description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
|
|
85
|
-
.option('--
|
|
86
|
-
.action(async (opts: {
|
|
87
|
-
await runBuildCatalog(opts.
|
|
85
|
+
.option('--mc-dir <path>', 'Path to the .mc registry directory (defaults to .mc in cwd)')
|
|
86
|
+
.action(async (opts: { mcDir?: string }) => {
|
|
87
|
+
await runBuildCatalog(opts.mcDir);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
program
|
package/src/commands/add-api.ts
CHANGED
|
@@ -96,7 +96,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
96
96
|
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const apiFilePath = join(domainDir, '
|
|
99
|
+
const apiFilePath = join(domainDir, 'actions', `${opts.id}.ts`);
|
|
100
100
|
|
|
101
101
|
// Refuse if API already exists
|
|
102
102
|
try {
|
|
@@ -124,7 +124,7 @@ export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
|
124
124
|
await writeFile(apiFilePath, apiContent);
|
|
125
125
|
|
|
126
126
|
// Create fixture with the input schema's default shape as the example payload
|
|
127
|
-
const apiTestDir = join(domainDir, '
|
|
127
|
+
const apiTestDir = join(domainDir, 'actions', '__tests__');
|
|
128
128
|
await mkdir(apiTestDir, { recursive: true });
|
|
129
129
|
await writeFile(
|
|
130
130
|
join(apiTestDir, `${opts.id}.fixture.json`),
|
|
@@ -51,18 +51,18 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Create directory structure
|
|
54
|
-
await mkdir(join(domainDir, '
|
|
54
|
+
await mkdir(join(domainDir, 'actions', '__tests__'), { recursive: true });
|
|
55
55
|
await mkdir(join(domainDir, 'subscribers'), { recursive: true });
|
|
56
56
|
await mkdir(join(domainDir, 'publishes'), { recursive: true });
|
|
57
57
|
|
|
58
58
|
// Write skeleton files
|
|
59
59
|
await writeFile(join(domainDir, 'domain.config.ts'), domainConfigTemplate(opts.id, opts.tenancy));
|
|
60
60
|
await writeFile(
|
|
61
|
-
join(domainDir, '
|
|
61
|
+
join(domainDir, 'actions', 'example.ts'),
|
|
62
62
|
apiSkeletonTemplate(opts.id, 'example', opts.tenancy)
|
|
63
63
|
);
|
|
64
64
|
await writeFile(
|
|
65
|
-
join(domainDir, '
|
|
65
|
+
join(domainDir, 'actions', '__tests__', 'example.fixture.json'),
|
|
66
66
|
apiFixtureSkeleton(opts.id, 'example')
|
|
67
67
|
);
|
|
68
68
|
await writeFile(join(domainDir, 'publishes', 'events.ts'), eventsSkeletonTemplate(opts.id));
|
|
@@ -94,7 +94,7 @@ export async function runAddDomain(opts: AddDomainOptions): Promise<void> {
|
|
|
94
94
|
cliLogger.info({ id: opts.id, dir: domainDir }, 'Domain scaffolded');
|
|
95
95
|
// eslint-disable-next-line no-console
|
|
96
96
|
console.log(
|
|
97
|
-
`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit
|
|
97
|
+
`\n✓ Domain "${opts.id}" added at ${domainDir}\nNext: edit actions/example.ts, ` +
|
|
98
98
|
`then run \`mc-domain-module build ${opts.id}\` to generate the registry.`
|
|
99
99
|
);
|
|
100
100
|
}
|
|
@@ -47,21 +47,20 @@ export async function runAddFixtureFactory(options: AddFixtureFactoryOptions): P
|
|
|
47
47
|
const domain = options.domain;
|
|
48
48
|
const apiId = options.apiId;
|
|
49
49
|
|
|
50
|
-
const apiFile = join(projectRoot, 'domains', domain, '
|
|
50
|
+
const apiFile = join(projectRoot, 'domains', domain, 'actions', `${apiId}.ts`);
|
|
51
51
|
await access(apiFile).catch(() => {
|
|
52
|
-
throw new Error(`
|
|
52
|
+
throw new Error(`Action file not found: ${apiFile}. Run add-api first.`);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// Read the
|
|
55
|
+
// Read the action file to extract the input type name
|
|
56
56
|
const content = await readFile(apiFile, 'utf8');
|
|
57
|
-
const outputTypeArg = content.match(/import\s+\{\s*[\w\s,]*\s*\}\s+from\s+['"]@mettlecast/);
|
|
58
57
|
|
|
59
|
-
const factoryFile = join(projectRoot, 'domains', domain, '
|
|
58
|
+
const factoryFile = join(projectRoot, 'domains', domain, 'actions', '__tests__', `${apiId}.factory.ts`);
|
|
60
59
|
const factoryContent = FACTORY_TEMPLATE
|
|
61
60
|
.replace(/\{domain\}/g, domain)
|
|
62
61
|
.replace(/\{apiId\}/g, apiId);
|
|
63
62
|
|
|
64
|
-
await mkdir(join(projectRoot, 'domains', domain, '
|
|
63
|
+
await mkdir(join(projectRoot, 'domains', domain, 'actions', '__tests__'), { recursive: true });
|
|
65
64
|
await writeFile(factoryFile, factoryContent, 'utf8');
|
|
66
65
|
|
|
67
66
|
cliLogger.info({ factoryFile }, 'add-fixture-factory: factory written');
|
|
@@ -2,27 +2,18 @@ import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { cliLogger } from '../utils/logger.js';
|
|
4
4
|
|
|
5
|
-
/** A single API entry in the catalog. */
|
|
6
|
-
export interface CatalogApi {
|
|
7
|
-
id: string;
|
|
8
|
-
domainId: string;
|
|
9
|
-
path: string;
|
|
10
|
-
method: string;
|
|
11
|
-
authType: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
requestSchema?: Record<string, unknown>;
|
|
14
|
-
responseSchema?: Record<string, unknown>;
|
|
15
|
-
versions?: Array<{ version: string; requestSchema?: Record<string, unknown>; responseSchema?: Record<string, unknown> }>;
|
|
16
|
-
examples?: { request?: Record<string, unknown>; response?: Record<string, unknown> };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
5
|
/** A single action entry in the catalog. */
|
|
20
6
|
export interface CatalogAction {
|
|
21
7
|
id: string;
|
|
22
8
|
domainId: string;
|
|
23
|
-
|
|
9
|
+
backendAccess: string;
|
|
10
|
+
exposure: Record<string, unknown>;
|
|
11
|
+
exposureDeclared?: boolean;
|
|
24
12
|
idempotent: boolean;
|
|
25
13
|
description?: string;
|
|
14
|
+
handlerFile?: string;
|
|
15
|
+
deployment?: Record<string, unknown>;
|
|
16
|
+
outboundAccess?: string;
|
|
26
17
|
inputSchema?: Record<string, unknown>;
|
|
27
18
|
outputSchema?: Record<string, unknown>;
|
|
28
19
|
}
|
|
@@ -83,7 +74,6 @@ export interface DomainCatalog {
|
|
|
83
74
|
version: 2;
|
|
84
75
|
generatedAt: string;
|
|
85
76
|
domains: CatalogDomain[];
|
|
86
|
-
apis: CatalogApi[];
|
|
87
77
|
actions: CatalogAction[];
|
|
88
78
|
events: CatalogEvent[];
|
|
89
79
|
subscribers: CatalogSubscriber[];
|
|
@@ -95,30 +85,29 @@ export interface DomainCatalog {
|
|
|
95
85
|
/**
|
|
96
86
|
* Build the combined domain catalog from all per-domain registry files.
|
|
97
87
|
* Reads .mc/{domain}-registry.json files and merges them into .mc/domain-registry.json.
|
|
98
|
-
* @param
|
|
88
|
+
* @param registryDir - Path to the .mc registry directory. Defaults to .mc in cwd.
|
|
99
89
|
* @returns The written catalog.
|
|
100
90
|
*/
|
|
101
|
-
export async function runBuildCatalog(
|
|
102
|
-
const dir =
|
|
91
|
+
export async function runBuildCatalog(registryDir?: string): Promise<DomainCatalog> {
|
|
92
|
+
const dir = registryDir ? resolve(registryDir) : join(process.cwd(), '.mc');
|
|
103
93
|
|
|
104
94
|
// Find all per-domain registry files
|
|
105
95
|
let files: string[];
|
|
106
96
|
try {
|
|
107
97
|
files = await readdir(dir);
|
|
108
98
|
} catch {
|
|
109
|
-
throw new Error(`build-catalog: .
|
|
99
|
+
throw new Error(`build-catalog: .mc registry directory not found at ${dir}. Run mc-domain-module build first.`);
|
|
110
100
|
}
|
|
111
101
|
|
|
112
102
|
const registryFiles = files.filter(f => f.endsWith('-registry.json') && f !== 'domain-registry.json');
|
|
113
103
|
if (registryFiles.length === 0) {
|
|
114
|
-
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run
|
|
104
|
+
throw new Error(`build-catalog: no domain registry files found in ${dir}. Run mc-domain-module build <domain> first.`);
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
const catalog: DomainCatalog = {
|
|
118
108
|
version: 2,
|
|
119
109
|
generatedAt: new Date().toISOString(),
|
|
120
110
|
domains: [],
|
|
121
|
-
apis: [],
|
|
122
111
|
actions: [],
|
|
123
112
|
events: [],
|
|
124
113
|
subscribers: [],
|
|
@@ -152,30 +141,19 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
|
|
|
152
141
|
description: domainEntry['description'] as string | undefined,
|
|
153
142
|
});
|
|
154
143
|
|
|
155
|
-
const apis = (registry['apis'] as Record<string, unknown>[] | undefined) ?? [];
|
|
156
|
-
for (const api of apis) {
|
|
157
|
-
catalog.apis.push({
|
|
158
|
-
id: String(api['id']),
|
|
159
|
-
domainId,
|
|
160
|
-
path: String(api['path']),
|
|
161
|
-
method: String(api['method'] ?? 'ANY'),
|
|
162
|
-
authType: String(api['authType'] ?? 'jwt'),
|
|
163
|
-
description: api['description'] as string | undefined,
|
|
164
|
-
requestSchema: api['requestSchema'] as Record<string, unknown> | undefined,
|
|
165
|
-
responseSchema: api['responseSchema'] as Record<string, unknown> | undefined,
|
|
166
|
-
versions: api['versions'] as CatalogApi['versions'],
|
|
167
|
-
examples: api['examples'] as CatalogApi['examples'],
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
144
|
const actions = (registry['actions'] as Record<string, unknown>[] | undefined) ?? [];
|
|
172
145
|
for (const action of actions) {
|
|
173
146
|
catalog.actions.push({
|
|
174
147
|
id: String(action['id']),
|
|
175
148
|
domainId,
|
|
176
|
-
|
|
149
|
+
backendAccess: String(action['backendAccess'] ?? 'private'),
|
|
150
|
+
exposure: (action['exposure'] as Record<string, unknown> | undefined) ?? { type: 'internal' },
|
|
151
|
+
exposureDeclared: action['exposureDeclared'] as boolean | undefined,
|
|
177
152
|
idempotent: Boolean(action['idempotent'] ?? false),
|
|
178
153
|
description: action['description'] as string | undefined,
|
|
154
|
+
handlerFile: action['handlerFile'] as string | undefined,
|
|
155
|
+
deployment: action['deployment'] as Record<string, unknown> | undefined,
|
|
156
|
+
outboundAccess: action['outboundAccess'] as string | undefined,
|
|
179
157
|
inputSchema: action['inputSchema'] as Record<string, unknown> | undefined,
|
|
180
158
|
outputSchema: action['outputSchema'] as Record<string, unknown> | undefined,
|
|
181
159
|
});
|
|
@@ -241,7 +219,7 @@ export async function runBuildCatalog(tibDir?: string): Promise<DomainCatalog> {
|
|
|
241
219
|
await writeFile(outPath, JSON.stringify(catalog), 'utf8');
|
|
242
220
|
|
|
243
221
|
cliLogger.info(
|
|
244
|
-
{ outPath, domains: catalog.domains.length,
|
|
222
|
+
{ outPath, domains: catalog.domains.length, actions: catalog.actions.length, events: catalog.events.length, jobs: catalog.jobs.length, schedules: catalog.schedules.length, integrations: catalog.integrations.length },
|
|
245
223
|
'Domain catalog written'
|
|
246
224
|
);
|
|
247
225
|
|
|
@@ -285,7 +285,7 @@ async function loadFlowsFromDir(dir: string, owningDomainOverride?: string): Pro
|
|
|
285
285
|
*/
|
|
286
286
|
export async function runBuildFlows(options: BuildFlowsOptions): Promise<string> {
|
|
287
287
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
288
|
-
const outFile = options.outFile ?? join(projectRoot, '.
|
|
288
|
+
const outFile = options.outFile ?? join(projectRoot, '.mc', 'flows-registry.json');
|
|
289
289
|
|
|
290
290
|
cliLogger.info({ projectRoot }, 'Building flows registry');
|
|
291
291
|
|
package/src/commands/build.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFile, mkdir, readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve, basename } from 'node:path';
|
|
3
|
-
import type { DomainRegistry } from '
|
|
3
|
+
import type { DomainRegistry } from '../types.js';
|
|
4
4
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
5
5
|
import { buildActionsTypes } from '../builder/build-types.js';
|
|
6
6
|
import { cliLogger } from '../utils/logger.js';
|
|
@@ -27,7 +27,7 @@ export async function runBuild(options: BuildOptions): Promise<string> {
|
|
|
27
27
|
const domainId = basename(domainRoot);
|
|
28
28
|
const outFile = options.outFile
|
|
29
29
|
? resolve(options.outFile)
|
|
30
|
-
: join(process.cwd(), '.
|
|
30
|
+
: join(process.cwd(), '.mc', `${domainId}-registry.json`);
|
|
31
31
|
|
|
32
32
|
cliLogger.info({ domainRoot }, 'Building domain registry');
|
|
33
33
|
|
|
@@ -40,18 +40,19 @@ export async function runBuild(options: BuildOptions): Promise<string> {
|
|
|
40
40
|
await mkdir(join(outFile, '..'), { recursive: true });
|
|
41
41
|
await writeFile(outFile, JSON.stringify(registry), 'utf8');
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
|
|
44
|
+
cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
|
|
44
45
|
|
|
45
46
|
// Discover all sibling registries and emit aggregated types
|
|
46
|
-
const
|
|
47
|
-
const registryFiles = (await readdir(
|
|
47
|
+
const registryDir = join(outFile, '..');
|
|
48
|
+
const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
|
|
48
49
|
const allRegistries: DomainRegistry[] = [];
|
|
49
50
|
for (const f of registryFiles) {
|
|
50
|
-
const raw = await readFile(join(
|
|
51
|
+
const raw = await readFile(join(registryDir, f), 'utf8');
|
|
51
52
|
allRegistries.push(JSON.parse(raw) as DomainRegistry);
|
|
52
53
|
}
|
|
53
54
|
const typesContent = buildActionsTypes(allRegistries);
|
|
54
|
-
const typesFile = join(
|
|
55
|
+
const typesFile = join(registryDir, 'actions-types.d.ts');
|
|
55
56
|
await writeFile(typesFile, typesContent);
|
|
56
57
|
|
|
57
58
|
cliLogger.info({ outFile: typesFile }, 'Types file written');
|
|
@@ -57,14 +57,14 @@ interface ModulesHashesManifest {
|
|
|
57
57
|
*/
|
|
58
58
|
export async function runCheckHashes(opts: CheckHashesOptions = {}): Promise<CheckHashesResult> {
|
|
59
59
|
const root = opts.projectRoot ?? process.cwd();
|
|
60
|
-
const manifestPath = join(root, '.
|
|
60
|
+
const manifestPath = join(root, '.mc', 'modules-hashes.json');
|
|
61
61
|
const modulesDir = join(root, 'infra', 'modules');
|
|
62
62
|
|
|
63
63
|
let manifest: ModulesHashesManifest;
|
|
64
64
|
try {
|
|
65
65
|
manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as ModulesHashesManifest;
|
|
66
66
|
} catch (err) {
|
|
67
|
-
throw new Error(`
|
|
67
|
+
throw new Error(`mc-domain-module check-hashes: cannot read ${manifestPath} (${(err as Error).message}). Re-scaffold to regenerate the manifest.`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const drifted: DriftedFile[] = [];
|
|
@@ -217,7 +217,7 @@ export async function runCreateProject(opts: CreateProjectOptions): Promise<void
|
|
|
217
217
|
|
|
218
218
|
// 5. Prepare output directory
|
|
219
219
|
await mkdir(outputDir, { recursive: true });
|
|
220
|
-
await mkdir(join(outputDir, '.
|
|
220
|
+
await mkdir(join(outputDir, '.mc'), { recursive: true });
|
|
221
221
|
|
|
222
222
|
// 6. Get CLI version from package.json
|
|
223
223
|
let cliVersion = '0.0.0';
|
package/src/commands/dev.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function startDev(options: DevOptions): Promise<DevHandle> {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
cliLogger.info(
|
|
58
|
-
{ domain: registry.domain.id,
|
|
58
|
+
{ domain: registry.domain.id, apiActions: registry.actions.filter(a => a.exposure?.type === 'api').length, port },
|
|
59
59
|
'Starting local dev server'
|
|
60
60
|
);
|
|
61
61
|
|