@pattern-stack/codegen 0.7.3 → 0.7.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,6 +64,12 @@ export const <%= entityNamePlural %> = pgTable(
64
64
  deletedAt: timestamp('deleted_at'),
65
65
  <%_ } _%>
66
66
  },
67
+ <%_ if (hasExternalIdTracking) { _%>
68
+ (t) => [
69
+ // external_id_tracking behavior — ON CONFLICT target for syncUpsert
70
+ uniqueIndex('uq_<%= entityNamePlural %>_provider_external_id').on(t.provider, t.externalId),
71
+ ],
72
+ <%_ } _%>
67
73
  );
68
74
  <%_ if (clpHasRelationsBlock) { _%>
69
75
  <%_ const needsMany = typeof clpExistingHasMany !== 'undefined' && clpExistingHasMany.length > 0; _%>
@@ -105,6 +105,38 @@ export function resolvePatternBaseClasses(entity) {
105
105
  };
106
106
  }
107
107
 
108
+ /**
109
+ * Resolve the behaviors implied by an entity's declared pattern(s).
110
+ *
111
+ * A pattern (e.g. `Synced`) may declare `impliedBehaviors` — behaviors the
112
+ * entity gets for free without re-declaring them in its `behaviors:` array.
113
+ * Walks every declared pattern (both the `pattern: X` and `patterns: [...]`
114
+ * shapes), unions their `impliedBehaviors`, and returns a deduped list.
115
+ * Unknown patterns are skipped silently — composition validation surfaces
116
+ * those separately (src/patterns/validate-composition.ts).
117
+ *
118
+ * @param {object} entity - the entity block from the parsed YAML
119
+ * @returns {string[]} deduped implied behavior names
120
+ */
121
+ export function resolveImpliedBehaviors(entity) {
122
+ const names = [];
123
+ if (typeof entity.pattern === 'string' && entity.pattern) {
124
+ names.push(entity.pattern);
125
+ }
126
+ if (Array.isArray(entity.patterns)) {
127
+ names.push(...entity.patterns);
128
+ }
129
+
130
+ const implied = new Set();
131
+ for (const name of names) {
132
+ const def = getPattern(name);
133
+ for (const b of def?.impliedBehaviors ?? []) {
134
+ implied.add(b);
135
+ }
136
+ }
137
+ return Array.from(implied);
138
+ }
139
+
108
140
  // ============================================================================
109
141
  // Helper utilities
110
142
  // ============================================================================
@@ -441,10 +473,13 @@ function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSof
441
473
  imports.add('timestamp');
442
474
  }
443
475
 
444
- // external_id_tracking behavior injects varchar + jsonb columns
476
+ // external_id_tracking behavior injects varchar + jsonb columns plus a
477
+ // unique index over (provider, external_id) — the ON CONFLICT target the
478
+ // sync sink's syncUpsert relies on.
445
479
  if (hasExternalIdTracking) {
446
480
  imports.add('varchar');
447
481
  imports.add('jsonb');
482
+ imports.add('uniqueIndex');
448
483
  }
449
484
 
450
485
  if (belongsTo.length > 0 || hasMany.length > 0) {
@@ -787,8 +822,20 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
787
822
  // Process entity fields
788
823
  const processedFields = processFields(fields);
789
824
 
790
- // Behavior flags (re-read from behaviors array for clean-lite-ps use)
791
- const behaviorNames = behaviors.map((b) => (typeof b === 'string' ? b : b.name));
825
+ // Behavior flags (re-read from behaviors array for clean-lite-ps use).
826
+ //
827
+ // Fold in the resolved pattern's `impliedBehaviors` (ADR-031): an entity
828
+ // declaring e.g. `pattern: Synced` need not re-declare the
829
+ // `external_id_tracking` behavior — the pattern contributes it. Deduped
830
+ // with any explicit `behaviors:` entries, explicit-first so order is
831
+ // stable for pre-existing fixtures. Mirrors the dedup in
832
+ // src/patterns/validate-composition.ts.
833
+ const explicitBehaviorNames = behaviors.map((b) => (typeof b === 'string' ? b : b.name));
834
+ const impliedBehaviorNames = resolveImpliedBehaviors(entity);
835
+ const behaviorNames = [
836
+ ...explicitBehaviorNames,
837
+ ...impliedBehaviorNames.filter((b) => !explicitBehaviorNames.includes(b)),
838
+ ];
792
839
  const hasTimestamps = behaviorNames.includes('timestamps');
793
840
  const hasSoftDelete = behaviorNames.includes('soft_delete');
794
841
  const hasUserTracking = behaviorNames.includes('user_tracking');