@pattern-stack/codegen 0.7.4 → 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.
- package/dist/runtime/base-classes/index.d.ts +2 -0
- package/dist/runtime/base-classes/index.js +345 -18
- package/dist/runtime/base-classes/index.js.map +1 -1
- package/dist/runtime/base-classes/junction-sync-repository.d.ts +87 -0
- package/dist/runtime/base-classes/junction-sync-repository.js +362 -0
- package/dist/runtime/base-classes/junction-sync-repository.js.map +1 -0
- package/dist/runtime/base-classes/sync-upsert-config.d.ts +58 -0
- package/dist/runtime/base-classes/sync-upsert-config.js +1 -0
- package/dist/runtime/base-classes/sync-upsert-config.js.map +1 -0
- package/dist/runtime/base-classes/synced-entity-repository.d.ts +68 -6
- package/dist/runtime/base-classes/synced-entity-repository.js +173 -7
- package/dist/runtime/base-classes/synced-entity-repository.js.map +1 -1
- package/dist/runtime/subsystems/sync/execute-sync.use-case.js +19 -1
- package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +1 -1
- package/dist/runtime/subsystems/sync/index.js +19 -1
- package/dist/runtime/subsystems/sync/index.js.map +1 -1
- package/dist/runtime/subsystems/sync/sync-sink.protocol.d.ts +6 -0
- package/dist/src/cli/index.js +24 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +21 -2
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/base-classes/index.ts +9 -0
- package/runtime/base-classes/junction-sync-repository.ts +284 -0
- package/runtime/base-classes/sync-upsert-config.ts +58 -0
- package/runtime/base-classes/synced-entity-repository.ts +263 -9
- package/runtime/subsystems/sync/execute-sync.use-case.ts +25 -1
- package/runtime/subsystems/sync/sync-sink.protocol.ts +7 -0
- package/src/patterns/library/synced.pattern.ts +2 -1
- package/templates/_shared/generated-banner.mjs +74 -0
- package/templates/broadcast/new/backend-interface.ejs.t +1 -0
- package/templates/broadcast/new/bridge-listener.ejs.t +1 -0
- package/templates/broadcast/new/channel.ejs.t +1 -0
- package/templates/broadcast/new/index.ejs.t +1 -0
- package/templates/broadcast/new/memory-backend.ejs.t +1 -0
- package/templates/broadcast/new/module.ejs.t +1 -0
- package/templates/broadcast/new/prompt.js +13 -0
- package/templates/broadcast/new/websocket-backend.ejs.t +1 -0
- package/templates/entity/new/backend/application/commands/create.ejs.t +1 -0
- package/templates/entity/new/backend/application/commands/delete.ejs.t +1 -0
- package/templates/entity/new/backend/application/commands/grouped-index.ejs.t +1 -0
- package/templates/entity/new/backend/application/commands/index.ejs.t +1 -0
- package/templates/entity/new/backend/application/commands/update.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/declarative-queries.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/index.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/list.ejs.t +1 -0
- package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +1 -0
- package/templates/entity/new/backend/application/schemas/dto.ejs.t +1 -0
- package/templates/entity/new/backend/database/repository.ejs.t +1 -0
- package/templates/entity/new/backend/database/schema.ejs.t +1 -0
- package/templates/entity/new/backend/domain/entity.ejs.t +1 -0
- package/templates/entity/new/backend/domain/grouped-index.ejs.t +1 -0
- package/templates/entity/new/backend/domain/index.ejs.t +1 -0
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +1 -0
- package/templates/entity/new/backend/modules/core/module.ejs.t +1 -0
- package/templates/entity/new/backend/modules/core/sync-source.ejs.t +1 -0
- package/templates/entity/new/backend/modules/core/sync-source.providers.ejs.t +1 -0
- package/templates/entity/new/backend/modules/trpc/module.ejs.t +1 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/dto/create.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/dto/output.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/dto/update.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/module.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +148 -0
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +92 -1
- package/templates/entity/new/clean-lite-ps/search-controller.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/service.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/create.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/declarative-queries.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/delete.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id-with-fields.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/list-with-fields.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/search.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/use-cases/update.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/collection.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/combined.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/fields.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/hooks.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/index.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/mutations.ejs.t +1 -0
- package/templates/entity/new/frontend/entity/types.ejs.t +1 -0
- package/templates/entity/new/frontend/store/hooks.ejs.t +1 -0
- package/templates/entity/new/frontend/unified-entity.ejs.t +1 -0
- package/templates/entity/new/prompt.js +19 -0
- package/templates/junction/new/entity.ejs.t +14 -2
- package/templates/junction/new/index.ejs.t +1 -0
- package/templates/junction/new/module.ejs.t +1 -0
- package/templates/junction/new/prompt.js +83 -0
- package/templates/junction/new/repository.ejs.t +44 -3
- package/templates/junction/new/service.ejs.t +1 -0
- package/templates/relationship/new/controller.ejs.t +1 -0
- package/templates/relationship/new/dto/create.ejs.t +1 -0
- package/templates/relationship/new/dto/output.ejs.t +1 -0
- package/templates/relationship/new/dto/update.ejs.t +1 -0
- package/templates/relationship/new/entity.ejs.t +1 -0
- package/templates/relationship/new/index.ejs.t +1 -0
- package/templates/relationship/new/module.ejs.t +1 -0
- package/templates/relationship/new/prompt.js +14 -0
- package/templates/relationship/new/repository.ejs.t +1 -0
- package/templates/relationship/new/service.ejs.t +1 -0
- package/templates/relationship/new/use-cases/declarative-queries.ejs.t +1 -0
- package/templates/relationship/new/use-cases/find-by-id.ejs.t +1 -0
- package/templates/relationship/new/use-cases/list.ejs.t +1 -0
- package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +1 -0
- package/templates/subsystem/auth/prompt.js +8 -0
- package/templates/subsystem/events/domain-events.schema.ejs.t +1 -0
- package/templates/subsystem/events/prompt.js +8 -0
- package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +1 -0
- package/templates/subsystem/jobs/prompt.js +8 -0
- package/templates/subsystem/sync/prompt.js +8 -0
- 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
|
|
@@ -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 %>,
|
|
@@ -45,8 +46,10 @@ export const <%= tableVarName %> = pgTable(
|
|
|
45
46
|
<%= rightColumnCamel %>: uuid('<%= rightColumn %>').notNull().references(() => <%= rightTable %>.id, { onDelete: '<%= onDeleteRight %>' }),
|
|
46
47
|
<%_ if (hasRole) { _%>
|
|
47
48
|
|
|
48
|
-
// Role enum (per-pairing; declared in junction YAML's fields.role.choices)
|
|
49
|
-
role
|
|
49
|
+
// Role enum (per-pairing; declared in junction YAML's fields.role.choices).
|
|
50
|
+
// NOT NULL because role is part of the junction's identity (composite PK
|
|
51
|
+
// below): the same pair with two different roles is two distinct rows.
|
|
52
|
+
role: <%= roleEnumName %>('role').notNull(),
|
|
50
53
|
<%_ } _%>
|
|
51
54
|
|
|
52
55
|
// BaseJunctionFields — is_primary is always emitted
|
|
@@ -83,8 +86,17 @@ export const <%= tableVarName %> = pgTable(
|
|
|
83
86
|
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
84
87
|
},
|
|
85
88
|
(table) => [
|
|
89
|
+
<%_ if (hasRole) { _%>
|
|
90
|
+
// Composite primary key on the two FK columns PLUS role (Q4 resolution: no
|
|
91
|
+
// surrogate id). Role is part of the junction's identity — the same pair
|
|
92
|
+
// with two different roles is two distinct rows (e.g. a contact who is both
|
|
93
|
+
// `champion` and `decision_maker` on one opportunity). This is the
|
|
94
|
+
// ON CONFLICT target for syncUpsert on role-bearing junctions.
|
|
95
|
+
primaryKey({ columns: [table.<%= leftColumnCamel %>, table.<%= rightColumnCamel %>, table.role] }),
|
|
96
|
+
<%_ } else { _%>
|
|
86
97
|
// Composite primary key on the two FK columns (Q4 resolution: no surrogate id)
|
|
87
98
|
primaryKey({ columns: [table.<%= leftColumnCamel %>, table.<%= rightColumnCamel %>] }),
|
|
99
|
+
<%_ } _%>
|
|
88
100
|
],
|
|
89
101
|
);
|
|
90
102
|
|