@pattern-stack/codegen 0.7.5 → 0.7.6

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.
Files changed (119) hide show
  1. package/dist/runtime/base-classes/index.d.ts +2 -0
  2. package/dist/runtime/base-classes/index.js +345 -18
  3. package/dist/runtime/base-classes/index.js.map +1 -1
  4. package/dist/runtime/base-classes/junction-sync-repository.d.ts +87 -0
  5. package/dist/runtime/base-classes/junction-sync-repository.js +362 -0
  6. package/dist/runtime/base-classes/junction-sync-repository.js.map +1 -0
  7. package/dist/runtime/base-classes/sync-upsert-config.d.ts +58 -0
  8. package/dist/runtime/base-classes/sync-upsert-config.js +1 -0
  9. package/dist/runtime/base-classes/sync-upsert-config.js.map +1 -0
  10. package/dist/runtime/base-classes/synced-entity-repository.d.ts +68 -6
  11. package/dist/runtime/base-classes/synced-entity-repository.js +173 -7
  12. package/dist/runtime/base-classes/synced-entity-repository.js.map +1 -1
  13. package/dist/runtime/subsystems/sync/execute-sync.use-case.js +19 -1
  14. package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +1 -1
  15. package/dist/runtime/subsystems/sync/index.js +19 -1
  16. package/dist/runtime/subsystems/sync/index.js.map +1 -1
  17. package/dist/runtime/subsystems/sync/sync-sink.protocol.d.ts +6 -0
  18. package/dist/src/cli/index.js +24 -2
  19. package/dist/src/cli/index.js.map +1 -1
  20. package/dist/src/index.js +21 -2
  21. package/dist/src/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/runtime/base-classes/index.ts +9 -0
  24. package/runtime/base-classes/junction-sync-repository.ts +284 -0
  25. package/runtime/base-classes/sync-upsert-config.ts +58 -0
  26. package/runtime/base-classes/synced-entity-repository.ts +263 -9
  27. package/runtime/subsystems/sync/execute-sync.use-case.ts +25 -1
  28. package/runtime/subsystems/sync/sync-sink.protocol.ts +7 -0
  29. package/src/patterns/library/synced.pattern.ts +2 -1
  30. package/templates/_shared/generated-banner.mjs +74 -0
  31. package/templates/broadcast/new/backend-interface.ejs.t +1 -0
  32. package/templates/broadcast/new/bridge-listener.ejs.t +1 -0
  33. package/templates/broadcast/new/channel.ejs.t +1 -0
  34. package/templates/broadcast/new/index.ejs.t +1 -0
  35. package/templates/broadcast/new/memory-backend.ejs.t +1 -0
  36. package/templates/broadcast/new/module.ejs.t +1 -0
  37. package/templates/broadcast/new/prompt.js +13 -0
  38. package/templates/broadcast/new/websocket-backend.ejs.t +1 -0
  39. package/templates/entity/new/backend/application/commands/create.ejs.t +1 -0
  40. package/templates/entity/new/backend/application/commands/delete.ejs.t +1 -0
  41. package/templates/entity/new/backend/application/commands/grouped-index.ejs.t +1 -0
  42. package/templates/entity/new/backend/application/commands/index.ejs.t +1 -0
  43. package/templates/entity/new/backend/application/commands/update.ejs.t +1 -0
  44. package/templates/entity/new/backend/application/queries/declarative-queries.ejs.t +1 -0
  45. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +1 -0
  46. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +1 -0
  47. package/templates/entity/new/backend/application/queries/index.ejs.t +1 -0
  48. package/templates/entity/new/backend/application/queries/list.ejs.t +1 -0
  49. package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +1 -0
  50. package/templates/entity/new/backend/application/schemas/dto.ejs.t +1 -0
  51. package/templates/entity/new/backend/database/repository.ejs.t +1 -0
  52. package/templates/entity/new/backend/database/schema.ejs.t +1 -0
  53. package/templates/entity/new/backend/domain/entity.ejs.t +1 -0
  54. package/templates/entity/new/backend/domain/grouped-index.ejs.t +1 -0
  55. package/templates/entity/new/backend/domain/index.ejs.t +1 -0
  56. package/templates/entity/new/backend/domain/repository-interface.ejs.t +1 -0
  57. package/templates/entity/new/backend/modules/core/module.ejs.t +1 -0
  58. package/templates/entity/new/backend/modules/core/sync-source.ejs.t +1 -0
  59. package/templates/entity/new/backend/modules/core/sync-source.providers.ejs.t +1 -0
  60. package/templates/entity/new/backend/modules/trpc/module.ejs.t +1 -0
  61. package/templates/entity/new/backend/presentation/controller.ejs.t +1 -0
  62. package/templates/entity/new/clean-lite-ps/controller.ejs.t +1 -0
  63. package/templates/entity/new/clean-lite-ps/dto/create.ejs.t +1 -0
  64. package/templates/entity/new/clean-lite-ps/dto/output.ejs.t +1 -0
  65. package/templates/entity/new/clean-lite-ps/dto/update.ejs.t +1 -0
  66. package/templates/entity/new/clean-lite-ps/entity.ejs.t +1 -0
  67. package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -0
  68. package/templates/entity/new/clean-lite-ps/module.ejs.t +1 -0
  69. package/templates/entity/new/clean-lite-ps/prompt-extension.js +148 -0
  70. package/templates/entity/new/clean-lite-ps/repository.ejs.t +92 -1
  71. package/templates/entity/new/clean-lite-ps/search-controller.ejs.t +1 -0
  72. package/templates/entity/new/clean-lite-ps/service.ejs.t +1 -0
  73. package/templates/entity/new/clean-lite-ps/use-cases/create.ejs.t +1 -0
  74. package/templates/entity/new/clean-lite-ps/use-cases/declarative-queries.ejs.t +1 -0
  75. package/templates/entity/new/clean-lite-ps/use-cases/delete.ejs.t +1 -0
  76. package/templates/entity/new/clean-lite-ps/use-cases/find-by-id-with-fields.ejs.t +1 -0
  77. package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +1 -0
  78. package/templates/entity/new/clean-lite-ps/use-cases/list-with-fields.ejs.t +1 -0
  79. package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +1 -0
  80. package/templates/entity/new/clean-lite-ps/use-cases/search.ejs.t +1 -0
  81. package/templates/entity/new/clean-lite-ps/use-cases/update.ejs.t +1 -0
  82. package/templates/entity/new/frontend/entity/collection.ejs.t +1 -0
  83. package/templates/entity/new/frontend/entity/combined.ejs.t +1 -0
  84. package/templates/entity/new/frontend/entity/fields.ejs.t +1 -0
  85. package/templates/entity/new/frontend/entity/hooks.ejs.t +1 -0
  86. package/templates/entity/new/frontend/entity/index.ejs.t +1 -0
  87. package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +1 -0
  88. package/templates/entity/new/frontend/entity/mutations.ejs.t +1 -0
  89. package/templates/entity/new/frontend/entity/types.ejs.t +1 -0
  90. package/templates/entity/new/frontend/store/hooks.ejs.t +1 -0
  91. package/templates/entity/new/frontend/unified-entity.ejs.t +1 -0
  92. package/templates/entity/new/prompt.js +19 -0
  93. package/templates/junction/new/entity.ejs.t +1 -0
  94. package/templates/junction/new/index.ejs.t +1 -0
  95. package/templates/junction/new/module.ejs.t +1 -0
  96. package/templates/junction/new/prompt.js +83 -0
  97. package/templates/junction/new/repository.ejs.t +44 -3
  98. package/templates/junction/new/service.ejs.t +1 -0
  99. package/templates/relationship/new/controller.ejs.t +1 -0
  100. package/templates/relationship/new/dto/create.ejs.t +1 -0
  101. package/templates/relationship/new/dto/output.ejs.t +1 -0
  102. package/templates/relationship/new/dto/update.ejs.t +1 -0
  103. package/templates/relationship/new/entity.ejs.t +1 -0
  104. package/templates/relationship/new/index.ejs.t +1 -0
  105. package/templates/relationship/new/module.ejs.t +1 -0
  106. package/templates/relationship/new/prompt.js +14 -0
  107. package/templates/relationship/new/repository.ejs.t +1 -0
  108. package/templates/relationship/new/service.ejs.t +1 -0
  109. package/templates/relationship/new/use-cases/declarative-queries.ejs.t +1 -0
  110. package/templates/relationship/new/use-cases/find-by-id.ejs.t +1 -0
  111. package/templates/relationship/new/use-cases/list.ejs.t +1 -0
  112. package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +1 -0
  113. package/templates/subsystem/auth/prompt.js +8 -0
  114. package/templates/subsystem/events/domain-events.schema.ejs.t +1 -0
  115. package/templates/subsystem/events/prompt.js +8 -0
  116. package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +1 -0
  117. package/templates/subsystem/jobs/prompt.js +8 -0
  118. package/templates/subsystem/sync/prompt.js +8 -0
  119. package/templates/subsystem/sync/sync-audit.schema.ejs.t +1 -0
@@ -722,6 +722,132 @@ function processSearchQueries(queriesBlock, processedFields, belongsTo, entityNa
722
722
  };
723
723
  }
724
724
 
725
+ // ============================================================================
726
+ // Sync write-surface derivation (#374)
727
+ // ============================================================================
728
+
729
+ /**
730
+ * Pre-compute the inbound-sync write surface for a `pattern: Synced` entity.
731
+ * Keeps the EJS thin + unit-testable: the template hand-emits the syncConfig
732
+ * literal (so `refTable` can carry a LIVE Drizzle table handle, which
733
+ * renderPatternConfigLiteral cannot express) using these locals.
734
+ *
735
+ * Returns null when the entity is not Synced.
736
+ *
737
+ * @param {string} patternName resolved pattern name
738
+ * @param {object[]} processedFields nonFkFields (camel + tsType + nullable)
739
+ * @param {object[]} belongsTo clpBelongsTo entries
740
+ * @param {boolean} hasTimestamps
741
+ * @param {boolean} eavEnabled
742
+ * @param {boolean} hasSoftDelete
743
+ */
744
+ export function buildSyncSurface(patternName, processedFields, belongsTo, hasTimestamps, eavEnabled, hasSoftDelete, fields) {
745
+ if (patternName !== 'Synced') return null;
746
+
747
+ // Copy-through columns: every non-FK declared field. external_id_tracking
748
+ // columns (external_id/provider/provider_metadata) are injected by the
749
+ // behavior, NOT present in processedFields, so they're already excluded.
750
+ const writeColumns = processedFields.map((f) => f.camelName);
751
+
752
+ // FK resolvers — one per belongs_to. writeKey = `${relationKey}ExternalId`
753
+ // (Decision 4). refTable is the string 'self' for self-FKs, else the parent
754
+ // table var name (emitted as a live identifier by the template).
755
+ const fkResolvers = belongsTo.map((rel) => ({
756
+ column: rel.camelField,
757
+ writeKey: `${rel.relationKey}ExternalId`,
758
+ refTable: rel.isSelfFk ? 'self' : rel.relatedTable,
759
+ isSelfFk: rel.isSelfFk,
760
+ nullable: rel.nullable,
761
+ // Strict resolution (throw on unresolved parent → failed item) when the FK
762
+ // COLUMN is required/non-null; opportunistic null otherwise. Sourced from
763
+ // the FK field's `required` — the relationship-level `nullable` is
764
+ // unreliable (defaults true when undeclared, e.g. a `belongs_to` with no
765
+ // explicit `nullable:`). Nullable FKs (e.g. self-FK hierarchies) stay
766
+ // opportunistic. (#374)
767
+ strict: fields?.[rel.field]?.required === true,
768
+ relatedTable: rel.relatedTable,
769
+ relatedEntity: rel.relatedEntity,
770
+ importPath: rel.importPath,
771
+ }));
772
+
773
+ // Projection columns: id + externalId + copy-through + local FK columns +
774
+ // timestamps. Omits provider/provider_metadata.
775
+ const projectionColumns = [
776
+ 'id',
777
+ 'externalId',
778
+ ...writeColumns,
779
+ ...belongsTo.map((rel) => rel.camelField),
780
+ ...(hasTimestamps ? ['createdAt', 'updatedAt'] : []),
781
+ ];
782
+
783
+ // The syncConfig object literal the template hand-emits. fkResolvers carry a
784
+ // sentinel so the template can swap `refTable` to either 'self' or the live
785
+ // table identifier.
786
+ const syncConfig = {
787
+ conflictTarget: ['provider', 'externalId'],
788
+ writeColumns,
789
+ projectionColumns,
790
+ eav: !!eavEnabled,
791
+ softDelete: !!hasSoftDelete,
792
+ };
793
+
794
+ // TSyncWrite fields: externalId:string, copy-through (typed, nullable-aware),
795
+ // one `<writeKey>?: string | null` per FK, fields?: Record<string, unknown>.
796
+ const writeFields = processedFields.map((f) => ({
797
+ camelName: f.camelName,
798
+ tsType: f.nullable ? `${f.tsType} | null` : f.tsType,
799
+ }));
800
+ const writeFkFields = fkResolvers.map((fk) => ({
801
+ name: fk.writeKey,
802
+ tsType: 'string | null',
803
+ }));
804
+
805
+ // TSyncProjection fields: id + externalId + copy-through (typed) + each local
806
+ // FK column (typed string, nullable per rel) + createdAt/updatedAt.
807
+ const projectionFields = [
808
+ { camelName: 'id', tsType: 'string' },
809
+ { camelName: 'externalId', tsType: 'string' },
810
+ ...processedFields.map((f) => ({
811
+ camelName: f.camelName,
812
+ tsType: f.nullable ? `${f.tsType} | null` : f.tsType,
813
+ })),
814
+ ...belongsTo.map((rel) => ({
815
+ camelName: rel.camelField,
816
+ tsType: rel.nullable ? 'string | null' : 'string',
817
+ })),
818
+ ...(hasTimestamps
819
+ ? [
820
+ { camelName: 'createdAt', tsType: 'Date' },
821
+ { camelName: 'updatedAt', tsType: 'Date' },
822
+ ]
823
+ : []),
824
+ ];
825
+
826
+ // Parent-table imports for non-self FKs, deduped (#368). Each entry imports
827
+ // the parent table var from its entity file. The entity's OWN table import is
828
+ // emitted separately by the template; we exclude self-FKs here.
829
+ const parentImportMap = new Map();
830
+ for (const fk of fkResolvers) {
831
+ if (fk.isSelfFk) continue;
832
+ if (!parentImportMap.has(fk.relatedTable)) {
833
+ parentImportMap.set(fk.relatedTable, {
834
+ table: fk.relatedTable,
835
+ importPath: fk.importPath,
836
+ });
837
+ }
838
+ }
839
+ const parentTableImports = Array.from(parentImportMap.values());
840
+
841
+ return {
842
+ syncConfig,
843
+ fkResolvers,
844
+ writeFields,
845
+ writeFkFields,
846
+ projectionFields,
847
+ parentTableImports,
848
+ };
849
+ }
850
+
725
851
  // ============================================================================
726
852
  // Main export
727
853
  // ============================================================================
@@ -1028,6 +1154,17 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
1028
1154
  zodChainOutput: zodChainForOutput(f),
1029
1155
  }));
1030
1156
 
1157
+ // Sync write-surface derivation (#374) — null unless pattern: Synced.
1158
+ const syncSurface = buildSyncSurface(
1159
+ patternName,
1160
+ nonFkFields,
1161
+ belongsTo,
1162
+ hasTimestamps,
1163
+ eavEnabled,
1164
+ hasSoftDelete,
1165
+ fields,
1166
+ );
1167
+
1031
1168
  // EVT-7: emits locals flow through from baseLocals (prompt.js computed them
1032
1169
  // against the full events registry). When this helper is called in isolation
1033
1170
  // (e.g. from unit tests) baseLocals.hasEmits may be undefined — provide
@@ -1072,6 +1209,17 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
1072
1209
  renderPatternConfigLiteral,
1073
1210
  ...patternConfigClasses,
1074
1211
 
1212
+ // Sync write-surface (#374) — emitted only for pattern: Synced. The
1213
+ // template hand-emits the syncConfig literal (live refTable handles) +
1214
+ // TSyncWrite/TSyncProjection from these.
1215
+ hasSyncSurface: syncSurface !== null,
1216
+ clpSyncConfig: syncSurface?.syncConfig ?? null,
1217
+ clpSyncFkResolvers: syncSurface?.fkResolvers ?? [],
1218
+ clpSyncWriteFields: syncSurface?.writeFields ?? [],
1219
+ clpSyncWriteFkFields: syncSurface?.writeFkFields ?? [],
1220
+ clpSyncProjectionFields: syncSurface?.projectionFields ?? [],
1221
+ clpSyncParentTableImports: syncSurface?.parentTableImports ?? [],
1222
+
1075
1223
  // Behavior flags (also exposed at top level for template use)
1076
1224
  hasTimestamps,
1077
1225
  hasSoftDelete,
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.repository : nul
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  import { Injectable, Inject } from '@nestjs/common';
7
8
  <%_
8
9
  // CGP-358: FK methods with opts take priority over same-named declarative query impl.
@@ -21,15 +22,65 @@ import { eq<%= hasMultiFieldQuery ? ', and' : '' %><%= hasOrderedQuery ? ', desc
21
22
  import { sql } from 'drizzle-orm';
22
23
  <% } -%>
23
24
  import { DRIZZLE } from '@shared/constants/tokens';
24
- import type { DrizzleClient<% if (eavValueTable) { %>, DrizzleTx<% } %> } from '@shared/types/drizzle';
25
+ import type { DrizzleClient<% if (eavValueTable || (typeof hasSyncSurface !== 'undefined' && hasSyncSurface)) { %>, DrizzleTx<% } %> } from '@shared/types/drizzle';
25
26
  import { <%= repositoryBaseClass %> } from '<%= repositoryBaseImport %>';
27
+ <% if (typeof hasSyncSurface !== 'undefined' && hasSyncSurface) { -%>
28
+ import type { SyncUpsertConfig } from '@shared/base-classes/sync-upsert-config';
29
+ <% } -%>
26
30
  <% if (hasTimestamps || hasSoftDelete || hasUserTracking) { -%>
27
31
  import type { BehaviorConfig } from '@shared/base-classes/base-repository';
28
32
  <% } -%>
33
+ <% if (eavEnabled) { -%>
34
+ import { FieldValueService } from '../field_values/field_value.service';
35
+ <% } -%>
29
36
  import { <%= entityNamePlural %>, type <%= classNames.entity %> } from './<%= entityName %>.entity';
37
+ <%_ if (typeof hasSyncSurface !== 'undefined' && hasSyncSurface) { _%>
38
+ <%_ clpSyncParentTableImports.forEach((imp) => { _%>
39
+ import { <%= imp.table %> } from '<%= imp.importPath %>';
40
+ <%_ }); _%>
41
+ <%_ } _%>
42
+ <%_ if (typeof hasSyncSurface !== 'undefined' && hasSyncSurface) { _%>
43
+
44
+ /**
45
+ * Canonical fields a synced <%= entityName %> write carries (#374). Copy-through
46
+ * columns are typed from the entity; each FK is named by its parent's external
47
+ * id and resolved <%= clpSyncFkResolvers.length > 0 ? 'in syncUpsertOne' : 'as configured' %>. Provider/providerMetadata are persistence
48
+ * seam, not carried here.
49
+ */
50
+ export interface <%= classNames.entity %>SyncWrite {
51
+ readonly externalId: string;
52
+ <%_ clpSyncWriteFields.forEach((f) => { _%>
53
+ readonly <%= f.camelName %>: <%- f.tsType %>;
54
+ <%_ }); _%>
55
+ <%_ clpSyncWriteFkFields.forEach((f) => { _%>
56
+ readonly <%= f.name %>?: <%- f.tsType %>;
57
+ <%_ }); _%>
58
+ /** Flat custom-field bag (EAV). */
59
+ readonly fields?: Record<string, unknown>;
60
+ }
61
+
62
+ /**
63
+ * Canonical-projected view of a <%= entityName %> row, keyed for the sync differ
64
+ * (#374). external_id_tracking columns (provider/providerMetadata) are OMITTED;
65
+ * externalId is kept.
66
+ */
67
+ export interface <%= classNames.entity %>SyncProjection {
68
+ <%_ clpSyncProjectionFields.forEach((f) => { _%>
69
+ readonly <%= f.camelName %>: <%- f.tsType %>;
70
+ <%_ }); _%>
71
+ }
72
+ <%_ } _%>
30
73
 
31
74
  @Injectable()
75
+ <%_ if (typeof hasSyncSurface !== 'undefined' && hasSyncSurface) { _%>
76
+ export class <%= classNames.repository %> extends <%= repositoryBaseClass %><
77
+ <%= classNames.entity %>,
78
+ <%= classNames.entity %>SyncWrite,
79
+ <%= classNames.entity %>SyncProjection
80
+ > {
81
+ <%_ } else { _%>
32
82
  export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%= classNames.entity %>> {
83
+ <%_ } _%>
33
84
  readonly table = <%= entityNamePlural %>;
34
85
  <% if (hasTimestamps || hasSoftDelete || hasUserTracking) { -%>
35
86
 
@@ -48,10 +99,50 @@ export class <%= classNames.repository %> extends <%= repositoryBaseClass %><<%=
48
99
  // runtime (identical shape to `behaviors: BehaviorConfig`).
49
100
  protected override readonly patternConfig = <%- renderPatternConfigLiteral(patternConfig, ' ', ' ') %> as const;
50
101
  <% } -%>
102
+ <%_ if (typeof hasSyncSurface !== 'undefined' && hasSyncSurface) { _%>
51
103
 
104
+ // Inbound-sync write surface (#374). Drives the generic syncUpsertOne /
105
+ // findByExternalIdProjected / softDeleteByExternalId on the base. FK
106
+ // resolvers carry LIVE Drizzle table handles ('self' → this.table).
107
+ protected readonly syncConfig: SyncUpsertConfig = {
108
+ conflictTarget: [<%- clpSyncConfig.conflictTarget.map((c) => `'${c}'`).join(', ') %>],
109
+ writeColumns: [<%- clpSyncConfig.writeColumns.map((c) => `'${c}'`).join(', ') %>],
110
+ fkResolvers: [
111
+ <%_ clpSyncFkResolvers.forEach((fk) => { _%>
112
+ { column: '<%= fk.column %>', writeKey: '<%= fk.writeKey %>', refTable: <%- fk.isSelfFk ? "'self'" : fk.refTable %><%= fk.strict ? ', strict: true' : '' %> },
113
+ <%_ }); _%>
114
+ ],
115
+ projectionColumns: [<%- clpSyncConfig.projectionColumns.map((c) => `'${c}'`).join(', ') %>],
116
+ eav: <%= clpSyncConfig.eav %>,
117
+ softDelete: <%= clpSyncConfig.softDelete %>,
118
+ };
119
+ <%_ } _%>
120
+
121
+ <%_ if (eavEnabled) { -%>
122
+ constructor(
123
+ @Inject(DRIZZLE) db: DrizzleClient,
124
+ private readonly fieldValues: FieldValueService,
125
+ ) {
126
+ super(db);
127
+ }
128
+
129
+ /**
130
+ * EAV dual-write override (#374 seam → #124 live path). Delegates to the
131
+ * shared FieldValueService so the inbound-sync write joins the same tx.
132
+ */
133
+ protected override async writeCustomFields(
134
+ db: DrizzleTx,
135
+ entityId: string,
136
+ userId: string,
137
+ fields: Record<string, unknown>,
138
+ ): Promise<void> {
139
+ await this.fieldValues.upsertFieldsTransactional('<%= entityName %>', entityId, userId, fields, db);
140
+ }
141
+ <%_ } else { -%>
52
142
  constructor(@Inject(DRIZZLE) db: DrizzleClient) {
53
143
  super(db);
54
144
  }
145
+ <%_ } -%>
55
146
  <% if (hasDeclarativeQueries) { -%>
56
147
 
57
148
  // ═══════════════════════════════════════════════════════════════════════
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.searchController
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.searchController %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  <% if (hasSearchQuery) { -%>
7
8
  import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
8
9
  import { z } from 'zod';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.service : null %
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  import { Injectable, Inject, Optional } from '@nestjs/common';
7
8
  import { WithAnalytics } from '@shared/base-classes/with-analytics';
8
9
  import { EVENT_BUS } from '@shared/constants/tokens';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.createUseCase :
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.createUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  <% if (eavEnabled) { -%>
7
8
  import { Injectable, Inject } from '@nestjs/common';
8
9
  import { DRIZZLE } from '<%= drizzleTokenImport %>';
@@ -2,6 +2,7 @@
2
2
  to: "<%= (typeof clpOutputPaths !== 'undefined' && hasDeclarativeQueries) ? clpOutputPaths.declarativeQueries : null %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  <% if (hasDeclarativeQueries) { -%>
6
7
  /**
7
8
  * Declarative Query Use Cases for <%= classNames.entity %>
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.deleteUseCase :
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.deleteUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  <% if (hasEmits && deleteEventType) { -%>
7
8
  import { Injectable, Inject, NotFoundException } from '@nestjs/common';
8
9
  import { DRIZZLE } from '<%= drizzleTokenImport %>';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.findByIdWithFiel
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.findByIdWithFieldsUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  import { Injectable } from '@nestjs/common';
7
8
  import { <%= classNames.service %> } from '../<%= entityName %>.service';
8
9
  import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
@@ -2,6 +2,7 @@
2
2
  to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.findByIdUseCase : null %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  import { Injectable, NotFoundException } from '@nestjs/common';
6
7
  import { <%= classNames.service %> } from '../<%= entityName %>.service';
7
8
  import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.listWithFieldsUs
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.listWithFieldsUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  import { Injectable } from '@nestjs/common';
7
8
  import { <%= classNames.service %> } from '../<%= entityName %>.service';
8
9
  import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
@@ -2,6 +2,7 @@
2
2
  to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.listUseCase : null %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  import { Injectable } from '@nestjs/common';
6
7
  import { <%= classNames.service %> } from '../<%= entityName %>.service';
7
8
  import type { <%= classNames.entity %> } from '../<%= entityName %>.entity';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.searchUseCase :
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.searchUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  <% if (hasSearchQuery) { -%>
7
8
  import { Injectable } from '@nestjs/common';
8
9
  import { and, asc, eq<% if (searchQuery.searchField) { %>, ilike<% } %>, type SQL } from 'drizzle-orm';
@@ -3,6 +3,7 @@ to: "<%= typeof clpOutputPaths !== 'undefined' ? clpOutputPaths.updateUseCase :
3
3
  skip_if: "<%= typeof clpOutputPaths === 'undefined' || !clpOutputPaths.updateUseCase %>"
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  <% if (eavEnabled) { -%>
7
8
  import { Injectable, Inject } from '@nestjs/common';
8
9
  import { DRIZZLE } from '<%= drizzleTokenImport %>';
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic') %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Collection
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'monolithic' ? `${locations.frontendGenerated.pa
3
3
  skip_if: <%= !frontendEnabled || (generate.structure !== 'monolithic') %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> - Generated Entity Module
8
9
  *
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic' || !generate.fieldMetadata) %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Field Metadata
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic') %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Hooks
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure !== 'entity-first') %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> - Entity Module
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic' || !generate.mutations || !(exposeTrpc || exposeRepository)) %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Mutation Hooks
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic' || !generate.mutations || !(exposeTrpc || exposeRepository)) %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Mutations
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: "<%= generate.structure === 'entity-first' ? `${locations.frontendGenerated.
3
3
  skip_if: <%= !frontendEnabled || (generate.structure === 'monolithic') %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Types
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: <%= locations.frontendStoreEntities.path %>/<%= name %>.ts
3
3
  skip_if: <%= !frontendEnabled || (!generate.hooks) %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> Entity Hooks
8
9
  * Generated by entity codegen - do not edit directly
@@ -3,6 +3,7 @@ to: <%= locations.frontendEntities.path %>/<%= name %>.ts
3
3
  skip_if: <%= !frontendEnabled %>
4
4
  force: true
5
5
  ---
6
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
7
8
  * <%= className %> - Unified Entity API
8
9
  *
@@ -24,6 +24,7 @@ import {
24
24
  getGenerateConfig,
25
25
  } from "../../../src/config/paths.mjs";
26
26
  import { getNamingConfig } from "../../../src/config/naming-config.mjs";
27
+ import { renderGeneratedBanner } from "../../_shared/generated-banner.mjs";
27
28
 
28
29
  // ============================================================================
29
30
  // Behavior Registry (inline to avoid import issues with Hygen)
@@ -1383,7 +1384,25 @@ export default {
1383
1384
  const drizzleTokenImport = '@shared/constants/tokens';
1384
1385
  const drizzleTypeImport = '@shared/types/drizzle';
1385
1386
 
1387
+ // @generated banner — single line stamped at the top of every
1388
+ // force-overwritten output. `yamlPath` is the consumer-relative source
1389
+ // (e.g. `entities/opportunity.yaml`). Extension seam differs by
1390
+ // architecture: clean-lite-ps customises via patterns, clean via the
1391
+ // base-class behavior config / repository subclass.
1392
+ const generatedBanner = renderGeneratedBanner({
1393
+ // Relative to cwd so the banner is portable across machines (an absolute
1394
+ // path would bake a developer's checkout root into every output).
1395
+ source: path.relative(process.cwd(), fullPath),
1396
+ generator: 'entity',
1397
+ seam: isCleanLitePs
1398
+ ? 'a pattern (src/patterns/*.pattern.ts) or the entity YAML'
1399
+ : 'the entity YAML or a base-class behavior config',
1400
+ });
1401
+
1386
1402
  const locals = {
1403
+ // @generated DO-NOT-EDIT banner (see renderGeneratedBanner)
1404
+ generatedBanner,
1405
+
1387
1406
  // Database configuration
1388
1407
  databaseDialect,
1389
1408
  schemaDir: BASE_PATHS.schemaDir,
@@ -2,6 +2,7 @@
2
2
  to: "<%= outputPaths.entity %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  import {
6
7
  <%_ drizzleImports.filter(i => i !== 'relations').forEach(i => { _%>
7
8
  <%= i %>,
@@ -2,6 +2,7 @@
2
2
  to: "<%= outputPaths.index %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  /**
6
7
  * <%= classNames.entity %> module barrel export
7
8
  * Generated by junction codegen — do not edit directly
@@ -2,6 +2,7 @@
2
2
  to: "<%= outputPaths.module %>"
3
3
  force: true
4
4
  ---
5
+ <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
5
6
  import { Module, forwardRef } from '@nestjs/common';
6
7
  import { DatabaseModule } from '@shared/database/database.module';
7
8