@pattern-stack/codegen 0.7.5 → 0.7.7

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 +356 -18
  3. package/dist/runtime/base-classes/index.js.map +1 -1
  4. package/dist/runtime/base-classes/junction-sync-repository.d.ts +89 -0
  5. package/dist/runtime/base-classes/junction-sync-repository.js +373 -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 +295 -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
@@ -2,7 +2,9 @@ export { BaseRepository, BehaviorConfig, ListOptions } from './base-repository.j
2
2
  export { BaseService, IBaseRepository } from './base-service.js';
3
3
  export { EventCategory, buildChangeEvents, buildLifecycleEvent, diffSnapshots, emitSafely, entitySnapshot } from './lifecycle-events.js';
4
4
  export { BaseFindByIdUseCase, BaseListUseCase, IFindByIdService, IListService } from './base-read-use-cases.js';
5
+ export { SyncFkResolver, SyncUpsertConfig } from './sync-upsert-config.js';
5
6
  export { SyncedEntityRepository } from './synced-entity-repository.js';
7
+ export { JunctionSyncConfig, JunctionSyncRepository, buildCompositeExternalId, parseCompositeExternalId } from './junction-sync-repository.js';
6
8
  export { ActivityEntityRepository } from './activity-entity-repository.js';
7
9
  export { MetadataEntityRepository } from './metadata-entity-repository.js';
8
10
  export { KnowledgeEntityRepository } from './knowledge-entity-repository.js';
@@ -81,8 +81,8 @@ var BaseRepository = class {
81
81
  if (conditions.length === 1) {
82
82
  query = query.where(conditions[0]);
83
83
  } else if (conditions.length > 1) {
84
- const { and: and2 } = await import("drizzle-orm");
85
- query = query.where(and2(...conditions));
84
+ const { and: and4 } = await import("drizzle-orm");
85
+ query = query.where(and4(...conditions));
86
86
  }
87
87
  const rows = await query;
88
88
  return rows[0]?.count ?? 0;
@@ -409,7 +409,7 @@ var BaseListUseCase = class {
409
409
  };
410
410
 
411
411
  // runtime/base-classes/synced-entity-repository.ts
412
- import { eq as eq2, inArray as inArray2 } from "drizzle-orm";
412
+ import { and, eq as eq2, inArray as inArray2 } from "drizzle-orm";
413
413
  var SyncedEntityRepository = class extends BaseRepository {
414
414
  /**
415
415
  * Find a single entity by its external CRM identifier.
@@ -433,12 +433,178 @@ var SyncedEntityRepository = class extends BaseRepository {
433
433
  const rows = await this.baseQuery().where(eq2(this.table["userId"], userId));
434
434
  return rows;
435
435
  }
436
+ // ==========================================================================
437
+ // Inbound sync (#374) — canonical→Drizzle write + provider-scoped FK
438
+ // resolution + EAV dual-write seam, all inside a SINGLE transaction.
439
+ // Driven entirely by `this.syncConfig`; the per-entity shape lives there.
440
+ // ==========================================================================
441
+ /**
442
+ * Upsert ONE entity by its `(provider, externalId)` identity, in a single
443
+ * transaction:
444
+ * 1. resolve each `syncConfig.fkResolvers` FK (provider-scoped). Strict
445
+ * resolvers throw on unresolved; non-strict leave the column null.
446
+ * 2. insert-or-update the canonical columns via `onConflictDoUpdate` on the
447
+ * `conflictTarget`. Resolved FKs are only written into `set` when
448
+ * non-null this run (no-clobber).
449
+ * 3. EAV dual-write of `write.fields` via `writeCustomFields` when
450
+ * `syncConfig.eav` and the bag is non-empty (same tx).
451
+ *
452
+ * Idempotent: a second call with the same identity updates in place. Returns
453
+ * the canonical projection (so the orchestrator records `local_id`).
454
+ *
455
+ * @param write canonical fields + parent external ids + custom-field bag
456
+ * @param provider adapter/provider label persisted + used to scope lookups
457
+ * @param tx optional outer transaction; when omitted we open our own
458
+ */
459
+ async syncUpsertOne(write, provider, tx) {
460
+ const cfg = this.syncConfig;
461
+ const w = write;
462
+ const run = async (db) => {
463
+ const resolvedFks = {};
464
+ for (const fk of cfg.fkResolvers) {
465
+ resolvedFks[fk.column] = await this.resolveFk(db, fk, w[fk.writeKey], provider);
466
+ }
467
+ const now = /* @__PURE__ */ new Date();
468
+ const copyThrough = {};
469
+ for (const col of cfg.writeColumns) copyThrough[col] = w[col];
470
+ const values = {
471
+ externalId: w["externalId"],
472
+ provider,
473
+ ...copyThrough,
474
+ ...resolvedFks,
475
+ ...this.behaviors.timestamps ? { updatedAt: now } : {}
476
+ };
477
+ const set = {
478
+ ...copyThrough,
479
+ ...this.behaviors.timestamps ? { updatedAt: now } : {}
480
+ };
481
+ for (const fk of cfg.fkResolvers) {
482
+ if (resolvedFks[fk.column] !== null) set[fk.column] = resolvedFks[fk.column];
483
+ }
484
+ const rows = await db.insert(this.table).values(values).onConflictDoUpdate({
485
+ target: cfg.conflictTarget.map((c) => this.table[c]),
486
+ set
487
+ }).returning();
488
+ const saved = rows[0];
489
+ const fields = w["fields"];
490
+ if (cfg.eav && fields && Object.keys(fields).length > 0) {
491
+ await this.writeCustomFields(
492
+ db,
493
+ saved["id"],
494
+ w["userId"],
495
+ fields
496
+ );
497
+ }
498
+ return this.toProjection(saved);
499
+ };
500
+ return tx ? run(tx) : this.db.transaction((t) => run(t));
501
+ }
436
502
  /**
437
- * Sync upsert bulk insert-or-update from external CRM data.
438
- * Concrete repositories must implement with the appropriate conflict target.
503
+ * Canonical-projected lookup by external id (differ-ready). Returns `null`
504
+ * when no local row exists. Provider-scoped so a HubSpot id can't match a
505
+ * Salesforce row.
439
506
  */
440
- async syncUpsert(_inputs) {
441
- throw new Error("syncUpsert not implemented \u2014 override in concrete repository");
507
+ async findByExternalIdProjected(externalId, provider) {
508
+ const rows = await this.db.select().from(this.table).where(
509
+ and(
510
+ eq2(this.table["provider"], provider),
511
+ eq2(this.table["externalId"], externalId)
512
+ )
513
+ ).limit(1);
514
+ const row = rows[0];
515
+ return row ? this.toProjection(row) : null;
516
+ }
517
+ /**
518
+ * Sync "delete" by external id, provider-scoped. When `softDelete: true`,
519
+ * sets `deletedAt`. When `softDelete: false`, tombstone-by-clearing: null out
520
+ * `external_id`/`provider` so the row no longer matches future inbound
521
+ * changes while preserving local-id references. Returns `{ id }` or `null`.
522
+ */
523
+ async softDeleteByExternalId(externalId, provider, tx) {
524
+ const db = this.runner(tx);
525
+ const set = this.syncConfig.softDelete ? { deletedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() } : { externalId: null, provider: null, updatedAt: /* @__PURE__ */ new Date() };
526
+ const rows = await db.update(this.table).set(set).where(
527
+ and(
528
+ eq2(this.table["provider"], provider),
529
+ eq2(this.table["externalId"], externalId)
530
+ )
531
+ ).returning({ id: this.table["id"] });
532
+ return rows[0] ? { id: rows[0].id } : null;
533
+ }
534
+ /**
535
+ * Batch sync upsert — concretizes the former abstract stub. Delegates to
536
+ * `syncUpsertOne` per input inside one transaction. Inputs are raw partial
537
+ * rows: provider is read from each input's own `provider` column; rows
538
+ * missing `externalId`/`provider` are skipped.
539
+ */
540
+ async syncUpsert(inputs) {
541
+ if (inputs.length === 0) return [];
542
+ return this.db.transaction(async (tx) => {
543
+ const out = [];
544
+ for (const input of inputs) {
545
+ const rec = input;
546
+ if (!rec["externalId"] || !rec["provider"]) continue;
547
+ const proj = await this.syncUpsertOne(
548
+ input,
549
+ rec["provider"],
550
+ tx
551
+ );
552
+ const id = proj["id"];
553
+ const row = await tx.select().from(this.table).where(eq2(this.table["id"], id)).limit(1);
554
+ out.push(row[0]);
555
+ }
556
+ return out;
557
+ });
558
+ }
559
+ /**
560
+ * Project a raw row to the canonical differ shape — a generic pick over
561
+ * `syncConfig.projectionColumns`. Override only for synthesized projections
562
+ * (e.g. junctions); entities use this verbatim.
563
+ */
564
+ toProjection(row) {
565
+ const r = row;
566
+ const out = {};
567
+ for (const col of this.syncConfig.projectionColumns) out[col] = r[col];
568
+ return out;
569
+ }
570
+ /**
571
+ * EAV dual-write seam (#374, live path lands in #124). No-op by default;
572
+ * `eav: true` entities emit a concrete override that injects
573
+ * `FieldValueService` and delegates to `upsertFieldsTransactional` so the
574
+ * dual-write joins the same tx (`db`). Kept as an explicit hook so the base
575
+ * stays portable (the FieldValueService dependency is eav-only).
576
+ */
577
+ async writeCustomFields(_db, _entityId, _userId, _fields) {
578
+ }
579
+ /**
580
+ * Resolve one FK from a parent external id (provider-scoped). `self` resolves
581
+ * against `this.table`. Strict resolvers throw when unresolved; non-strict
582
+ * return null. A null/absent write value short-circuits to null.
583
+ */
584
+ async resolveFk(db, fk, rawExternalId, provider) {
585
+ const parentExternalId = rawExternalId;
586
+ if (!parentExternalId) {
587
+ if (fk.strict) {
588
+ throw new Error(
589
+ `${this.constructor.name}.syncUpsertOne: missing required parent external id for '${fk.column}' (writeKey '${fk.writeKey}')`
590
+ );
591
+ }
592
+ return null;
593
+ }
594
+ const refTable = fk.refTable === "self" ? this.table : fk.refTable;
595
+ const rows = await db.select({ id: refTable["id"] }).from(refTable).where(
596
+ and(
597
+ eq2(refTable["provider"], provider),
598
+ eq2(refTable["externalId"], parentExternalId)
599
+ )
600
+ ).limit(1);
601
+ const id = rows[0]?.id ?? null;
602
+ if (id === null && fk.strict) {
603
+ throw new Error(
604
+ `${this.constructor.name}.syncUpsertOne: unresolved parent '${parentExternalId}' (provider '${provider}') for '${fk.column}' \u2014 parent not synced yet`
605
+ );
606
+ }
607
+ return id;
442
608
  }
443
609
  /**
444
610
  * Find entities visible to a user (ownership + sharing rules).
@@ -449,8 +615,177 @@ var SyncedEntityRepository = class extends BaseRepository {
449
615
  }
450
616
  };
451
617
 
618
+ // runtime/base-classes/junction-sync-repository.ts
619
+ import { and as and2, eq as eq3 } from "drizzle-orm";
620
+ var JunctionSyncRepository = class extends BaseRepository {
621
+ /**
622
+ * Upsert ONE junction row by its composite identity, in a single transaction:
623
+ * 1. resolve the REQUIRED left FK (provider-scoped) — STRICT: missing → throws;
624
+ * 2. resolve the REQUIRED right FK (provider-scoped) — STRICT: missing → throws;
625
+ * 3. insert-or-update on the `(left, right[, role])` conflict target.
626
+ *
627
+ * Idempotent. Returns the composite externalId as the projection `id`.
628
+ *
629
+ * @param write parent external ids (`<left>ExternalId`/`<right>ExternalId`)
630
+ * + optional `role` + `userId`
631
+ * @param provider adapter/provider label used to scope the parent lookups
632
+ * @param tx optional outer transaction; when omitted we open our own
633
+ */
634
+ async syncUpsertOne(write, provider, tx) {
635
+ const cfg = this.syncConfig;
636
+ const w = write;
637
+ const leftWriteKey = `${cfg.left.column.replace(/Id$/, "")}ExternalId`;
638
+ const rightWriteKey = `${cfg.right.column.replace(/Id$/, "")}ExternalId`;
639
+ const run = async (db) => {
640
+ const leftId = await this.resolveStrict(
641
+ db,
642
+ cfg.left.refTable,
643
+ w[leftWriteKey],
644
+ provider,
645
+ cfg.left.column
646
+ );
647
+ const rightId = await this.resolveStrict(
648
+ db,
649
+ cfg.right.refTable,
650
+ w[rightWriteKey],
651
+ provider,
652
+ cfg.right.column
653
+ );
654
+ const now = /* @__PURE__ */ new Date();
655
+ const role = cfg.roleColumn ? w["role"] : void 0;
656
+ const values = {
657
+ [cfg.left.column]: leftId,
658
+ [cfg.right.column]: rightId,
659
+ ...cfg.roleColumn ? { [cfg.roleColumn]: role } : {},
660
+ ...this.behaviors.timestamps ? { updatedAt: now } : {}
661
+ };
662
+ const target = cfg.roleColumn ? [this.table[cfg.left.column], this.table[cfg.right.column], this.table[cfg.roleColumn]] : [this.table[cfg.left.column], this.table[cfg.right.column]];
663
+ const rows = await db.insert(this.table).values(values).onConflictDoUpdate({
664
+ target,
665
+ set: { ...this.behaviors.timestamps ? { updatedAt: now } : {} }
666
+ }).returning();
667
+ const saved = rows[0];
668
+ return this.toProjection(saved, w, provider);
669
+ };
670
+ return tx ? run(tx) : this.db.transaction((t) => run(t));
671
+ }
672
+ /**
673
+ * Canonical-projected lookup by the COMPOSITE externalId, differ-ready. Parses
674
+ * the composite, resolves BOTH parents NON-throwing (→ null), then selects by
675
+ * the identity tuple. Returns `null` on malformed composite / unresolved
676
+ * parent / no row (a missing "before" side is a create from the differ's view).
677
+ */
678
+ async findByExternalIdProjected(externalId, provider) {
679
+ const cfg = this.syncConfig;
680
+ const parsed = parseCompositeExternalId(externalId, cfg.roleColumn !== null);
681
+ if (!parsed) return null;
682
+ const leftId = await this.resolveLoose(this.db, cfg.left.refTable, parsed.left, provider);
683
+ if (leftId === null) return null;
684
+ const rightId = await this.resolveLoose(this.db, cfg.right.refTable, parsed.right, provider);
685
+ if (rightId === null) return null;
686
+ const rows = await this.db.select().from(this.table).where(this.identityWhere(leftId, rightId, parsed.role)).limit(1);
687
+ const row = rows[0];
688
+ if (!row) return null;
689
+ const w = {
690
+ [`${cfg.left.column.replace(/Id$/, "")}ExternalId`]: parsed.left,
691
+ [`${cfg.right.column.replace(/Id$/, "")}ExternalId`]: parsed.right,
692
+ ...cfg.roleColumn ? { role: parsed.role } : {},
693
+ userId: ""
694
+ };
695
+ return this.toProjection(row, w, provider);
696
+ }
697
+ /**
698
+ * Hard-delete the junction by composite externalId. Junctions have no
699
+ * `deleted_at` and no external-linkage columns to clear, so a sync "delete"
700
+ * removes the row. Resolves both parents NON-throwing, then deletes by the
701
+ * identity tuple. Returns the composite id, or `null` when nothing matched.
702
+ */
703
+ async softDeleteByExternalId(externalId, provider, tx) {
704
+ const cfg = this.syncConfig;
705
+ const parsed = parseCompositeExternalId(externalId, cfg.roleColumn !== null);
706
+ if (!parsed) return null;
707
+ const db = this.runner(tx);
708
+ const leftId = await this.resolveLoose(db, cfg.left.refTable, parsed.left, provider);
709
+ if (leftId === null) return null;
710
+ const rightId = await this.resolveLoose(db, cfg.right.refTable, parsed.right, provider);
711
+ if (rightId === null) return null;
712
+ const rows = await db.delete(this.table).where(this.identityWhere(leftId, rightId, parsed.role)).returning({ id: this.table[cfg.left.column] });
713
+ return rows[0] ? { id: externalId } : null;
714
+ }
715
+ /**
716
+ * Project a raw junction row to the differ shape: the COMPOSITE externalId
717
+ * `id` (junctions have no surrogate id) plus the local FK columns, role (when
718
+ * role-bearing) and timestamps — matching the emitted
719
+ * `<Junction>SyncProjection` interface (id + leftId + rightId + role? +
720
+ * createdAt + updatedAt). Junction projections are purely structural, so this
721
+ * is fully generic over `syncConfig` — no per-junction override is emitted.
722
+ */
723
+ toProjection(row, write, _provider) {
724
+ const cfg = this.syncConfig;
725
+ const r = row;
726
+ const leftExt = write[`${cfg.left.column.replace(/Id$/, "")}ExternalId`];
727
+ const rightExt = write[`${cfg.right.column.replace(/Id$/, "")}ExternalId`];
728
+ const role = cfg.roleColumn ? r[cfg.roleColumn] : void 0;
729
+ const out = {
730
+ id: buildCompositeExternalId(leftExt, rightExt, role),
731
+ [cfg.left.column]: r[cfg.left.column],
732
+ [cfg.right.column]: r[cfg.right.column]
733
+ };
734
+ if (cfg.roleColumn) out[cfg.roleColumn] = r[cfg.roleColumn];
735
+ out["createdAt"] = r["createdAt"];
736
+ out["updatedAt"] = r["updatedAt"];
737
+ return out;
738
+ }
739
+ /** Build the identity WHERE clause `(left, right[, role])`. */
740
+ identityWhere(leftId, rightId, role) {
741
+ const cfg = this.syncConfig;
742
+ const conds = [
743
+ eq3(this.table[cfg.left.column], leftId),
744
+ eq3(this.table[cfg.right.column], rightId)
745
+ ];
746
+ if (cfg.roleColumn && role !== void 0) {
747
+ conds.push(eq3(this.table[cfg.roleColumn], role));
748
+ }
749
+ return and2(...conds);
750
+ }
751
+ /** Resolve a parent id (provider-scoped), throwing when unresolved. */
752
+ async resolveStrict(db, refTable, parentExternalId, provider, column) {
753
+ const id = await this.resolveLoose(db, refTable, parentExternalId, provider);
754
+ if (!id) {
755
+ throw new Error(
756
+ `${this.constructor.name}.syncUpsertOne: unresolved parent '${parentExternalId}' (provider '${provider}') for '${column}' \u2014 parent not synced yet`
757
+ );
758
+ }
759
+ return id;
760
+ }
761
+ /** Resolve a parent id (provider-scoped), returning null when unresolved. */
762
+ async resolveLoose(db, refTable, parentExternalId, provider) {
763
+ if (!parentExternalId) return null;
764
+ const rows = await db.select({ id: refTable["id"] }).from(refTable).where(
765
+ and2(
766
+ eq3(refTable["provider"], provider),
767
+ eq3(refTable["externalId"], parentExternalId)
768
+ )
769
+ ).limit(1);
770
+ return rows[0]?.id ?? null;
771
+ }
772
+ };
773
+ function buildCompositeExternalId(leftExternalId, rightExternalId, role) {
774
+ return role !== void 0 ? `${leftExternalId}::${rightExternalId}::${role}` : `${leftExternalId}::${rightExternalId}`;
775
+ }
776
+ function parseCompositeExternalId(externalId, withRole) {
777
+ const parts = externalId.split("::");
778
+ const expected = withRole ? 3 : 2;
779
+ if (parts.length !== expected || parts.some((p) => p.length === 0)) return null;
780
+ return {
781
+ left: parts[0],
782
+ right: parts[1],
783
+ role: withRole ? parts[2] : void 0
784
+ };
785
+ }
786
+
452
787
  // runtime/base-classes/activity-entity-repository.ts
453
- import { eq as eq3, between, desc } from "drizzle-orm";
788
+ import { eq as eq4, between, desc } from "drizzle-orm";
454
789
  var ActivityEntityRepository = class extends BaseRepository {
455
790
  /**
456
791
  * Find activities within a date range (inclusive).
@@ -463,27 +798,27 @@ var ActivityEntityRepository = class extends BaseRepository {
463
798
  * Find all activities for a specific user.
464
799
  */
465
800
  async findByUserId(userId) {
466
- const rows = await this.baseQuery().where(eq3(this.table["userId"], userId));
801
+ const rows = await this.baseQuery().where(eq4(this.table["userId"], userId));
467
802
  return rows;
468
803
  }
469
804
  /**
470
805
  * Find all activities for a specific opportunity.
471
806
  */
472
807
  async findByOpportunityId(opportunityId) {
473
- const rows = await this.baseQuery().where(eq3(this.table["opportunityId"], opportunityId));
808
+ const rows = await this.baseQuery().where(eq4(this.table["opportunityId"], opportunityId));
474
809
  return rows;
475
810
  }
476
811
  /**
477
812
  * Find the most recent activities for an opportunity, ordered by occurredAt desc.
478
813
  */
479
814
  async findRecentByOpportunityId(opportunityId, limit = 10) {
480
- const rows = await this.baseQuery().where(eq3(this.table["opportunityId"], opportunityId)).orderBy(desc(this.table["occurredAt"])).limit(limit);
815
+ const rows = await this.baseQuery().where(eq4(this.table["opportunityId"], opportunityId)).orderBy(desc(this.table["occurredAt"])).limit(limit);
481
816
  return rows;
482
817
  }
483
818
  };
484
819
 
485
820
  // runtime/base-classes/metadata-entity-repository.ts
486
- import { eq as eq4, and, desc as desc2 } from "drizzle-orm";
821
+ import { eq as eq5, and as and3, desc as desc2 } from "drizzle-orm";
487
822
  var MetadataEntityRepository = class extends BaseRepository {
488
823
  /**
489
824
  * Bulk upsert with a caller-specified conflict target.
@@ -510,9 +845,9 @@ var MetadataEntityRepository = class extends BaseRepository {
510
845
  */
511
846
  async findByEntityIdAndType(entityId, entityType) {
512
847
  const rows = await this.baseQuery().where(
513
- and(
514
- eq4(this.table["entityId"], entityId),
515
- eq4(this.table["entityType"], entityType)
848
+ and3(
849
+ eq5(this.table["entityId"], entityId),
850
+ eq5(this.table["entityType"], entityType)
516
851
  )
517
852
  );
518
853
  return rows;
@@ -521,14 +856,14 @@ var MetadataEntityRepository = class extends BaseRepository {
521
856
  * List all metadata records for an entity.
522
857
  */
523
858
  async listByEntityId(entityId) {
524
- const rows = await this.baseQuery().where(eq4(this.table["entityId"], entityId));
859
+ const rows = await this.baseQuery().where(eq5(this.table["entityId"], entityId));
525
860
  return rows;
526
861
  }
527
862
  /**
528
863
  * List metadata history for an entity, ordered by validFrom descending.
529
864
  */
530
865
  async listHistoryByEntityId(entityId) {
531
- const rows = await this.baseQuery().where(eq4(this.table["entityId"], entityId)).orderBy(desc2(this.table["validFrom"]));
866
+ const rows = await this.baseQuery().where(eq5(this.table["entityId"], entityId)).orderBy(desc2(this.table["validFrom"]));
532
867
  return rows;
533
868
  }
534
869
  };
@@ -644,6 +979,7 @@ export {
644
979
  BaseListUseCase,
645
980
  BaseRepository,
646
981
  BaseService,
982
+ JunctionSyncRepository,
647
983
  KnowledgeEntityRepository,
648
984
  KnowledgeEntityService,
649
985
  MetadataEntityRepository,
@@ -652,9 +988,11 @@ export {
652
988
  SyncedEntityService,
653
989
  WithAnalytics,
654
990
  buildChangeEvents,
991
+ buildCompositeExternalId,
655
992
  buildLifecycleEvent,
656
993
  diffSnapshots,
657
994
  emitSafely,
658
- entitySnapshot
995
+ entitySnapshot,
996
+ parseCompositeExternalId
659
997
  };
660
998
  //# sourceMappingURL=index.js.map