@mettlecast/domain-cli 0.2.64 → 0.2.66

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### fix(#4810): `build` no longer crashes on aggregate/flows registries
6
+
7
+ `runBuild` aggregated **every** `.mc/*-registry.json` file for `actions-types.d.ts`
8
+ generation, including the aggregate `domain-registry.json` and the
9
+ `flows-registry.json` (which have no `domain`/`actions` shape). This threw
10
+ `TypeError: Cannot read properties of undefined (reading 'id')` in
11
+ `buildActionsTypes`, breaking `mc-domain-module build` (and, downstream, the
12
+ `doctor` "Actions have types" check) on any scaffolded project that has flows
13
+ or an aggregate registry. The build now only aggregates per-domain registries
14
+ (those carrying `domain.id` and an `actions` array), and `buildActionsTypes`
15
+ defensively skips malformed registries.
16
+
5
17
  ### feat(#2132): Policy-as-data refactor (manifest v2)
6
18
 
7
19
  **Manifest schema bumped to v2.** Each `ManifestFileEntry` now carries a `policy` field:
@@ -9,6 +9,13 @@
9
9
  export function buildActionsTypes(registries) {
10
10
  const overloads = [];
11
11
  for (const reg of registries) {
12
+ // Skip registries that are not per-domain registries. The `.mc/` directory
13
+ // also contains aggregate (`domain-registry.json`) and flows
14
+ // (`flows-registry.json`) snapshots that share the `*-registry.json` suffix
15
+ // but have a different shape (no `domain` object / no `actions` array).
16
+ if (!reg?.domain?.id || !Array.isArray(reg.actions)) {
17
+ continue;
18
+ }
12
19
  for (const action of reg.actions) {
13
20
  const fqn = `${reg.domain.id}.${action.id}`;
14
21
  // Phase 1: use unknown for input/output
@@ -25,13 +25,29 @@ export async function runBuild(options) {
25
25
  await writeFile(outFile, JSON.stringify(registry), 'utf8');
26
26
  const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
27
27
  cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
28
- // Discover all sibling registries and emit aggregated types
28
+ // Discover all sibling per-domain registries and emit aggregated types.
29
+ // The `.mc/` directory also holds aggregate (`domain-registry.json`) and
30
+ // flows (`flows-registry.json`) snapshots that share the `*-registry.json`
31
+ // suffix but are not per-domain registries; only registries carrying a
32
+ // `domain.id` and an `actions` array are aggregated for type generation.
29
33
  const registryDir = join(outFile, '..');
30
34
  const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
31
35
  const allRegistries = [];
32
36
  for (const f of registryFiles) {
33
37
  const raw = await readFile(join(registryDir, f), 'utf8');
34
- allRegistries.push(JSON.parse(raw));
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ }
42
+ catch {
43
+ cliLogger.warn(`Skipping unparseable registry file: ${f}`);
44
+ continue;
45
+ }
46
+ const candidate = parsed;
47
+ if (!candidate?.domain?.id || !Array.isArray(candidate.actions)) {
48
+ continue;
49
+ }
50
+ allRegistries.push(candidate);
35
51
  }
36
52
  const typesContent = buildActionsTypes(allRegistries);
37
53
  const typesFile = join(registryDir, 'actions-types.d.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mettlecast/domain-cli",
3
- "version": "0.2.64",
3
+ "version": "0.2.66",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -71,6 +71,35 @@ describe('buildActionsTypes', () => {
71
71
  expect(output).toContain("call(actionId: 'payments.charge'");
72
72
  });
73
73
 
74
+ it('skips aggregate/flows registries that lack a domain object', () => {
75
+ const auth: DomainRegistry = {
76
+ schemaVersion: '1',
77
+ domainRoot: '/test/auth',
78
+ domain: { id: 'auth', kind: 'domain', name: 'Auth', tenancy: 'required' },
79
+ webhooks: [],
80
+ subscribers: [],
81
+ schedules: [],
82
+ jobs: [],
83
+ actions: [
84
+ { id: 'verify', kind: 'action', handlerFile: 'verify.ts', backendAccess: 'platform', exposure: { type: 'internal' }, idempotent: true },
85
+ ],
86
+ integrations: [],
87
+ events: [],
88
+ };
89
+
90
+ // Aggregate registry (domain-registry.json): has actions but no domain.
91
+ const aggregate = { schemaVersion: '1', actions: [{ id: 'x' }] } as unknown as DomainRegistry;
92
+ // Flows registry (flows-registry.json): neither domain nor actions.
93
+ const flows = { schemaVersion: '1', flows: [] } as unknown as DomainRegistry;
94
+
95
+ expect(() => buildActionsTypes([auth, aggregate, flows])).not.toThrow();
96
+
97
+ const output = buildActionsTypes([auth, aggregate, flows]);
98
+ expect(output).toContain("call(actionId: 'auth.verify'");
99
+ // The aggregate registry's bare action must not leak in without a domain prefix.
100
+ expect(output).not.toContain('undefined.x');
101
+ });
102
+
74
103
  it('returns valid TypeScript .d.ts syntax', () => {
75
104
  const registry: DomainRegistry = {
76
105
  schemaVersion: '1',
@@ -12,6 +12,13 @@ export function buildActionsTypes(registries: DomainRegistry[]): string {
12
12
  const overloads: string[] = [];
13
13
 
14
14
  for (const reg of registries) {
15
+ // Skip registries that are not per-domain registries. The `.mc/` directory
16
+ // also contains aggregate (`domain-registry.json`) and flows
17
+ // (`flows-registry.json`) snapshots that share the `*-registry.json` suffix
18
+ // but have a different shape (no `domain` object / no `actions` array).
19
+ if (!reg?.domain?.id || !Array.isArray(reg.actions)) {
20
+ continue;
21
+ }
15
22
  for (const action of reg.actions) {
16
23
  const fqn = `${reg.domain.id}.${action.id}`;
17
24
  // Phase 1: use unknown for input/output
@@ -43,13 +43,28 @@ export async function runBuild(options: BuildOptions): Promise<string> {
43
43
  const apiExposedActions = registry.actions.filter(action => action.exposure.type === 'api').length;
44
44
  cliLogger.info({ outFile, apiExposedActions, events: registry.events.length }, 'Registry written');
45
45
 
46
- // Discover all sibling registries and emit aggregated types
46
+ // Discover all sibling per-domain registries and emit aggregated types.
47
+ // The `.mc/` directory also holds aggregate (`domain-registry.json`) and
48
+ // flows (`flows-registry.json`) snapshots that share the `*-registry.json`
49
+ // suffix but are not per-domain registries; only registries carrying a
50
+ // `domain.id` and an `actions` array are aggregated for type generation.
47
51
  const registryDir = join(outFile, '..');
48
52
  const registryFiles = (await readdir(registryDir)).filter(f => f.endsWith('-registry.json'));
49
53
  const allRegistries: DomainRegistry[] = [];
50
54
  for (const f of registryFiles) {
51
55
  const raw = await readFile(join(registryDir, f), 'utf8');
52
- allRegistries.push(JSON.parse(raw) as DomainRegistry);
56
+ let parsed: unknown;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ } catch {
60
+ cliLogger.warn(`Skipping unparseable registry file: ${f}`);
61
+ continue;
62
+ }
63
+ const candidate = parsed as Partial<DomainRegistry>;
64
+ if (!candidate?.domain?.id || !Array.isArray(candidate.actions)) {
65
+ continue;
66
+ }
67
+ allRegistries.push(candidate as DomainRegistry);
53
68
  }
54
69
  const typesContent = buildActionsTypes(allRegistries);
55
70
  const typesFile = join(registryDir, 'actions-types.d.ts');