@pattern-stack/codegen 0.7.1 → 0.7.3

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
@@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.3] — 2026-05-23
8
+
9
+ Auto-emits `<generated>/subsystems.ts` — a `SUBSYSTEM_MODULES` barrel of `forRoot()` calls for every installed subsystem. Removes the "did I forget to wire `SyncModule`?" class of silent-failure bug when a subsystem is declared in `subsystems.install` but never imported into AppModule.
10
+
11
+ ### Added
12
+
13
+ - **`feat(codegen)` — subsystem composition barrel.** New `src/cli/shared/subsystem-barrel-generator.ts` emits `<generated>/subsystems.ts`:
14
+
15
+ ```ts
16
+ // AUTO-GENERATED — wire into AppModule once:
17
+ // @Module({ imports: [DatabaseModule, ...SUBSYSTEM_MODULES, ...GENERATED_MODULES] })
18
+ import type { DynamicModule } from '@nestjs/common';
19
+ import { EventsModule } from '../shared/subsystems/events/events.module';
20
+ import { JobsDomainModule } from '../shared/subsystems/jobs/jobs-domain.module';
21
+ import { JobWorkerModule } from '../shared/subsystems/jobs/job-worker.module';
22
+ import { BridgeModule } from '../shared/subsystems/bridge/bridge.module';
23
+ import { SyncModule } from '../shared/subsystems/sync/sync.module';
24
+
25
+ export const SUBSYSTEM_MODULES: DynamicModule[] = [
26
+ EventsModule.forRoot({ backend: 'drizzle', multiTenant: false }),
27
+ JobsDomainModule.forRoot({ backend: 'drizzle', multiTenant: false }),
28
+ JobWorkerModule.forRoot({ mode: 'embedded' }),
29
+ BridgeModule.forRoot({ backend: 'drizzle', multiTenant: false }),
30
+ SyncModule.forRoot({ backend: 'drizzle', multiTenant: false }),
31
+ ];
32
+ ```
33
+
34
+ Composer coverage: `events`, `jobs` (+ `JobWorkerModule` when `worker_mode: 'embedded'`), `bridge`, `sync`. `auth`, `auth-integrations`, `observability` are out of scope (their `forRoot` shapes take init-time arguments — encryption keys, IUserContext adapters — that can't be synthesized from config alone; hand-wire those).
35
+
36
+ - **Auto-regen wiring.** `codegen entity new --all` and `codegen subsystem install <name>` now call `regenerateSubsystemBarrel({ ctx, generatedDir })` after their existing barrel work. Soft-fail (warn-only) to match the entity-barrel pattern.
37
+
38
+ - **8 unit tests** under `src/__tests__/cli/subsystem-barrel-generator.test.ts` covering empty install set, single-subsystem composition, full-minimum-set ordering, `worker_mode: 'embedded'` toggle, `multi_tenant` propagation, unsupported subsystem reporting via `skipped`, default options when config block is missing.
39
+
40
+ ### Migration
41
+
42
+ Greenfield: re-run `codegen entity new --all` and add `...SUBSYSTEM_MODULES` to your AppModule's `imports`. The barrel is opt-in — existing AppModules continue to work without it.
43
+
44
+ The 4 subsystems composer currently supports are the ones consumers actually wire today; widening the composer set is straightforward — see `COMPOSERS` map in `subsystem-barrel-generator.ts`.
45
+
46
+ ### Known interaction
47
+
48
+ If your project uses `SyncModule.forRoot({ backend: 'drizzle', multiTenant: false })`, tsc will surface pre-existing `tenantId`-reference errors in `sync-cursor-store.drizzle-backend.ts` / `sync-run-recorder.drizzle-backend.ts` when the subsystem barrel transitively pulls those files into typecheck scope. This is an unrelated upstream issue (multi-tenancy-off + drizzle backend wasn't typecheck-clean before this PR either; consumers worked around it by excluding `src/shared/subsystems` from tsconfig). Either keep the exclude in place, or set `multi_tenant: true` in your sync config to add the `tenant_id` column the backends reference.
49
+
50
+ ## [0.7.2] — 2026-05-23
51
+
52
+ Hotfix for the sync subsystem differ — `external_id_tracking` columns (added by the `external_id_tracking` behavior) were always emitting field diffs even when they hadn't changed in vendor input, producing churn. (Shipped on `main` between 0.7.1 and the junction-import dedupe; previously unreleased.)
53
+
7
54
  ## [0.7.1] — 2026-05-23
8
55
 
9
56
  Hotfix for the junction emit pipeline (CGP-60, shipped in 0.7.0). When a parent entity participated in **multiple junctions** (e.g. `contact` appearing as the right side of both `account_contact` and `opportunity_contact`), each junction's `_inject-parent-{service,module}-import-clp-{left,right}.ejs.t` template emitted the same shared `import { forwardRef } from '@nestjs/common';` line independently — producing duplicate-import TS errors (TS2300 `Duplicate identifier 'forwardRef'`). The same loop also re-emitted the counterparty entity type import (`import type { Account } from '../accounts/account.entity'`), which collided with the parent's own `belongs_to` import emitted by `service.ejs.t`.
@@ -19,6 +19,10 @@ var DEFAULT_IGNORE_FIELDS = /* @__PURE__ */ new Set([
19
19
  "type",
20
20
  "lastModifiedAt",
21
21
  "fields",
22
+ "external_id",
23
+ "externalId",
24
+ "provider",
25
+ "provider_metadata",
22
26
  "providerMetadata"
23
27
  ]);
24
28
  var DeepEqualDiffer = class {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/sync/deep-equal.differ.ts"],"sourcesContent":["/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the sync subsystem (SYNC-5).\n *\n * Walks every field of `incoming` against `existing`, emitting a structured\n * per-field diff (`{ from, to }`) for every field whose value changed.\n * Returns `'noop'` when the record is unchanged.\n *\n * Design decisions (extracted from the upstream consumer + HS-9 findings):\n *\n * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally\n * so upstream cannot reasonably disagree:\n * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,\n * `lastModifiedAt`, `fields`, `providerMetadata`\n * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write\n * path, not at the canonical-record layer.)\n *\n * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the\n * comparison to the hinted field set. The hint is advisory; fields in\n * the ignore list are still filtered out even when hinted. Provider\n * hints are field-NAME-level; they don't override the ignore rules.\n *\n * 3. **Date → ISO string** — `Date` instances are normalized to\n * `toISOString()` before comparison. Sinks return `Date` from the DB\n * driver; adapters typically deliver strings. Direct `===` would\n * always say \"changed.\"\n *\n * 4. **Decimal-string vs number** — Postgres `numeric` columns return as\n * strings through Drizzle; adapters deliver numbers. When one side is a\n * number and the other is a numeric string that parses to the same\n * number, they're equal. The normalizer does NOT coerce non-numeric\n * strings, and it preserves zero-vs-null distinction.\n *\n * 5. **null-existing path** — `diff(null, incoming)` produces a full\n * created-shape diff (`{from: null, to: <value>}` for every non-ignored\n * field). Orchestrator sees this and records `operation: 'created'`.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n DiffResult,\n FieldDiff,\n IFieldDiffer,\n} from './sync-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in sync with consumer canonical-record shapes —\n * adding a row-metadata field here means no sync will ever mark it changed.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['sync_version']` etc. to augment the base list; values here are\n * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.\n */\n readonly ignore?: readonly string[];\n}\n\n@Injectable()\nexport class DeepEqualDiffer<T extends Record<string, unknown>>\n implements IFieldDiffer<T>\n{\n private readonly ignore: ReadonlySet<string>;\n\n constructor(opts: DeepEqualDifferOptions = {}) {\n if (opts.ignore && opts.ignore.length > 0) {\n this.ignore = new Set([...DEFAULT_IGNORE_FIELDS, ...opts.ignore]);\n } else {\n this.ignore = DEFAULT_IGNORE_FIELDS;\n }\n }\n\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult {\n // Created-shape: every non-ignored field becomes `{from: null, to}`.\n if (existing === null) {\n const out: FieldDiff = {};\n for (const key of Object.keys(incoming)) {\n if (this.ignore.has(key)) continue;\n const value = (incoming as Record<string, unknown>)[key];\n // Skip fields that are themselves null/undefined — a created record\n // doesn't need to declare \"this field is null now\" for every\n // untouched column.\n if (value === null || value === undefined) continue;\n out[key] = { from: null, to: value };\n }\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n\n // Field set to compare. `providerChangedFields` narrows to a hint set;\n // ignored fields are filtered out regardless of hint.\n const candidates = new Set<string>();\n if (providerChangedFields && providerChangedFields.length > 0) {\n for (const key of providerChangedFields) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n } else {\n for (const key of Object.keys(incoming)) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n // Also include keys that exist on existing but not on incoming —\n // e.g. a field that was cleared. This would otherwise be missed when\n // incoming carries an undefined column we drop from the iteration.\n for (const key of Object.keys(existing)) {\n if (this.ignore.has(key)) continue;\n if (!(key in (incoming as Record<string, unknown>))) continue;\n candidates.add(key);\n }\n }\n\n const out: FieldDiff = {};\n for (const key of candidates) {\n const before = (existing as Record<string, unknown>)[key];\n const after = (incoming as Record<string, unknown>)[key];\n if (!isEqual(before, after)) {\n out[key] = { from: before ?? null, to: after ?? null };\n }\n }\n\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n}\n\n// ─── equality helpers ───────────────────────────────────────────────────────\n\n/**\n * Field-level equality with the canonical-sync normalizations:\n * - Date → toISOString (adapters deliver strings)\n * - numeric-string vs number → numeric equality when both parse\n * - deep equality for plain objects/arrays (single-level is enough for\n * canonical records; nested records travel as jsonb columns where the\n * sink already owns the comparison)\n */\nfunction isEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n const na = normalize(a);\n const nb = normalize(b);\n if (na === nb) return true;\n\n // After normalization: both may still be non-primitive objects.\n if (\n typeof na === 'object' &&\n typeof nb === 'object' &&\n na !== null &&\n nb !== null\n ) {\n return deepEqualObject(na as Record<string, unknown>, nb as Record<string, unknown>);\n }\n\n // Numeric string ↔ number: when one side is a number and the other is a\n // string that parses to the same finite number.\n const numericEqual = maybeNumericEqual(na, nb) || maybeNumericEqual(nb, na);\n return numericEqual;\n}\n\nfunction normalize(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString();\n return value;\n}\n\nfunction maybeNumericEqual(a: unknown, b: unknown): boolean {\n // a is string-shape, b is number — parse a and compare. Only when the\n // string looks numeric AND the parse round-trips (no silent NaN pass-\n // through on non-numeric strings).\n if (typeof a !== 'string' || typeof b !== 'number') return false;\n if (a.trim() === '') return false;\n const parsed = Number(a);\n if (!Number.isFinite(parsed)) return false;\n return parsed === b;\n}\n\nfunction deepEqualObject(\n a: Record<string, unknown>,\n b: Record<string, unknown>,\n): boolean {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!(key in b)) return false;\n if (!isEqual(a[key], b[key])) return false;\n }\n return true;\n}\n"],"mappings":"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","names":["out"]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/sync/deep-equal.differ.ts"],"sourcesContent":["/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the sync subsystem (SYNC-5).\n *\n * Walks every field of `incoming` against `existing`, emitting a structured\n * per-field diff (`{ from, to }`) for every field whose value changed.\n * Returns `'noop'` when the record is unchanged.\n *\n * Design decisions (extracted from the upstream consumer + HS-9 findings):\n *\n * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally\n * so upstream cannot reasonably disagree:\n * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,\n * `lastModifiedAt`, `fields`, `providerMetadata`\n * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write\n * path, not at the canonical-record layer.)\n *\n * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the\n * comparison to the hinted field set. The hint is advisory; fields in\n * the ignore list are still filtered out even when hinted. Provider\n * hints are field-NAME-level; they don't override the ignore rules.\n *\n * 3. **Date → ISO string** — `Date` instances are normalized to\n * `toISOString()` before comparison. Sinks return `Date` from the DB\n * driver; adapters typically deliver strings. Direct `===` would\n * always say \"changed.\"\n *\n * 4. **Decimal-string vs number** — Postgres `numeric` columns return as\n * strings through Drizzle; adapters deliver numbers. When one side is a\n * number and the other is a numeric string that parses to the same\n * number, they're equal. The normalizer does NOT coerce non-numeric\n * strings, and it preserves zero-vs-null distinction.\n *\n * 5. **null-existing path** — `diff(null, incoming)` produces a full\n * created-shape diff (`{from: null, to: <value>}` for every non-ignored\n * field). Orchestrator sees this and records `operation: 'created'`.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n DiffResult,\n FieldDiff,\n IFieldDiffer,\n} from './sync-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in sync with consumer canonical-record shapes —\n * adding a row-metadata field here means no sync will ever mark it changed.\n *\n * Includes the columns contributed by the `external_id_tracking` behavior\n * (`external_id`/`externalId`, `provider`, `provider_metadata`/`providerMetadata`).\n * These are sync-tracking metadata, not domain attributes: they ride on the\n * canonical record but must never register as a field change (the external id\n * is the record's identity, not a mutable value). Listed in both snake_case\n * and camelCase so the differ ignores them regardless of the consumer's\n * canonical projection casing.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'external_id',\n 'externalId',\n 'provider',\n 'provider_metadata',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['sync_version']` etc. to augment the base list; values here are\n * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.\n */\n readonly ignore?: readonly string[];\n}\n\n@Injectable()\nexport class DeepEqualDiffer<T extends Record<string, unknown>>\n implements IFieldDiffer<T>\n{\n private readonly ignore: ReadonlySet<string>;\n\n constructor(opts: DeepEqualDifferOptions = {}) {\n if (opts.ignore && opts.ignore.length > 0) {\n this.ignore = new Set([...DEFAULT_IGNORE_FIELDS, ...opts.ignore]);\n } else {\n this.ignore = DEFAULT_IGNORE_FIELDS;\n }\n }\n\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult {\n // Created-shape: every non-ignored field becomes `{from: null, to}`.\n if (existing === null) {\n const out: FieldDiff = {};\n for (const key of Object.keys(incoming)) {\n if (this.ignore.has(key)) continue;\n const value = (incoming as Record<string, unknown>)[key];\n // Skip fields that are themselves null/undefined — a created record\n // doesn't need to declare \"this field is null now\" for every\n // untouched column.\n if (value === null || value === undefined) continue;\n out[key] = { from: null, to: value };\n }\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n\n // Field set to compare. `providerChangedFields` narrows to a hint set;\n // ignored fields are filtered out regardless of hint.\n const candidates = new Set<string>();\n if (providerChangedFields && providerChangedFields.length > 0) {\n for (const key of providerChangedFields) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n } else {\n for (const key of Object.keys(incoming)) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n // Also include keys that exist on existing but not on incoming —\n // e.g. a field that was cleared. This would otherwise be missed when\n // incoming carries an undefined column we drop from the iteration.\n for (const key of Object.keys(existing)) {\n if (this.ignore.has(key)) continue;\n if (!(key in (incoming as Record<string, unknown>))) continue;\n candidates.add(key);\n }\n }\n\n const out: FieldDiff = {};\n for (const key of candidates) {\n const before = (existing as Record<string, unknown>)[key];\n const after = (incoming as Record<string, unknown>)[key];\n if (!isEqual(before, after)) {\n out[key] = { from: before ?? null, to: after ?? null };\n }\n }\n\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n}\n\n// ─── equality helpers ───────────────────────────────────────────────────────\n\n/**\n * Field-level equality with the canonical-sync normalizations:\n * - Date → toISOString (adapters deliver strings)\n * - numeric-string vs number → numeric equality when both parse\n * - deep equality for plain objects/arrays (single-level is enough for\n * canonical records; nested records travel as jsonb columns where the\n * sink already owns the comparison)\n */\nfunction isEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n const na = normalize(a);\n const nb = normalize(b);\n if (na === nb) return true;\n\n // After normalization: both may still be non-primitive objects.\n if (\n typeof na === 'object' &&\n typeof nb === 'object' &&\n na !== null &&\n nb !== null\n ) {\n return deepEqualObject(na as Record<string, unknown>, nb as Record<string, unknown>);\n }\n\n // Numeric string ↔ number: when one side is a number and the other is a\n // string that parses to the same finite number.\n const numericEqual = maybeNumericEqual(na, nb) || maybeNumericEqual(nb, na);\n return numericEqual;\n}\n\nfunction normalize(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString();\n return value;\n}\n\nfunction maybeNumericEqual(a: unknown, b: unknown): boolean {\n // a is string-shape, b is number — parse a and compare. Only when the\n // string looks numeric AND the parse round-trips (no silent NaN pass-\n // through on non-numeric strings).\n if (typeof a !== 'string' || typeof b !== 'number') return false;\n if (a.trim() === '') return false;\n const parsed = Number(a);\n if (!Number.isFinite(parsed)) return false;\n return parsed === b;\n}\n\nfunction deepEqualObject(\n a: Record<string, unknown>,\n b: Record<string, unknown>,\n): boolean {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!(key in b)) return false;\n if (!isEqual(a[key], b[key])) return false;\n }\n return true;\n}\n"],"mappings":"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","names":["out"]}
@@ -624,6 +624,10 @@ var DEFAULT_IGNORE_FIELDS = /* @__PURE__ */ new Set([
624
624
  "type",
625
625
  "lastModifiedAt",
626
626
  "fields",
627
+ "external_id",
628
+ "externalId",
629
+ "provider",
630
+ "provider_metadata",
627
631
  "providerMetadata"
628
632
  ]);
629
633
  var DeepEqualDiffer = class {