@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
@@ -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,166 @@ 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. `id` is the COMPOSITE
717
+ * externalId (the junction has no surrogate id). Override to widen the
718
+ * projection beyond the identity tuple (the template emits a concrete
719
+ * `toProjection` carrying the role + local FKs + timestamps).
720
+ */
721
+ toProjection(_row, write, _provider) {
722
+ const cfg = this.syncConfig;
723
+ const leftExt = write[`${cfg.left.column.replace(/Id$/, "")}ExternalId`];
724
+ const rightExt = write[`${cfg.right.column.replace(/Id$/, "")}ExternalId`];
725
+ const role = cfg.roleColumn ? write["role"] : void 0;
726
+ return { id: buildCompositeExternalId(leftExt, rightExt, role) };
727
+ }
728
+ /** Build the identity WHERE clause `(left, right[, role])`. */
729
+ identityWhere(leftId, rightId, role) {
730
+ const cfg = this.syncConfig;
731
+ const conds = [
732
+ eq3(this.table[cfg.left.column], leftId),
733
+ eq3(this.table[cfg.right.column], rightId)
734
+ ];
735
+ if (cfg.roleColumn && role !== void 0) {
736
+ conds.push(eq3(this.table[cfg.roleColumn], role));
737
+ }
738
+ return and2(...conds);
739
+ }
740
+ /** Resolve a parent id (provider-scoped), throwing when unresolved. */
741
+ async resolveStrict(db, refTable, parentExternalId, provider, column) {
742
+ const id = await this.resolveLoose(db, refTable, parentExternalId, provider);
743
+ if (!id) {
744
+ throw new Error(
745
+ `${this.constructor.name}.syncUpsertOne: unresolved parent '${parentExternalId}' (provider '${provider}') for '${column}' \u2014 parent not synced yet`
746
+ );
747
+ }
748
+ return id;
749
+ }
750
+ /** Resolve a parent id (provider-scoped), returning null when unresolved. */
751
+ async resolveLoose(db, refTable, parentExternalId, provider) {
752
+ if (!parentExternalId) return null;
753
+ const rows = await db.select({ id: refTable["id"] }).from(refTable).where(
754
+ and2(
755
+ eq3(refTable["provider"], provider),
756
+ eq3(refTable["externalId"], parentExternalId)
757
+ )
758
+ ).limit(1);
759
+ return rows[0]?.id ?? null;
760
+ }
761
+ };
762
+ function buildCompositeExternalId(leftExternalId, rightExternalId, role) {
763
+ return role !== void 0 ? `${leftExternalId}::${rightExternalId}::${role}` : `${leftExternalId}::${rightExternalId}`;
764
+ }
765
+ function parseCompositeExternalId(externalId, withRole) {
766
+ const parts = externalId.split("::");
767
+ const expected = withRole ? 3 : 2;
768
+ if (parts.length !== expected || parts.some((p) => p.length === 0)) return null;
769
+ return {
770
+ left: parts[0],
771
+ right: parts[1],
772
+ role: withRole ? parts[2] : void 0
773
+ };
774
+ }
775
+
452
776
  // runtime/base-classes/activity-entity-repository.ts
453
- import { eq as eq3, between, desc } from "drizzle-orm";
777
+ import { eq as eq4, between, desc } from "drizzle-orm";
454
778
  var ActivityEntityRepository = class extends BaseRepository {
455
779
  /**
456
780
  * Find activities within a date range (inclusive).
@@ -463,27 +787,27 @@ var ActivityEntityRepository = class extends BaseRepository {
463
787
  * Find all activities for a specific user.
464
788
  */
465
789
  async findByUserId(userId) {
466
- const rows = await this.baseQuery().where(eq3(this.table["userId"], userId));
790
+ const rows = await this.baseQuery().where(eq4(this.table["userId"], userId));
467
791
  return rows;
468
792
  }
469
793
  /**
470
794
  * Find all activities for a specific opportunity.
471
795
  */
472
796
  async findByOpportunityId(opportunityId) {
473
- const rows = await this.baseQuery().where(eq3(this.table["opportunityId"], opportunityId));
797
+ const rows = await this.baseQuery().where(eq4(this.table["opportunityId"], opportunityId));
474
798
  return rows;
475
799
  }
476
800
  /**
477
801
  * Find the most recent activities for an opportunity, ordered by occurredAt desc.
478
802
  */
479
803
  async findRecentByOpportunityId(opportunityId, limit = 10) {
480
- const rows = await this.baseQuery().where(eq3(this.table["opportunityId"], opportunityId)).orderBy(desc(this.table["occurredAt"])).limit(limit);
804
+ const rows = await this.baseQuery().where(eq4(this.table["opportunityId"], opportunityId)).orderBy(desc(this.table["occurredAt"])).limit(limit);
481
805
  return rows;
482
806
  }
483
807
  };
484
808
 
485
809
  // runtime/base-classes/metadata-entity-repository.ts
486
- import { eq as eq4, and, desc as desc2 } from "drizzle-orm";
810
+ import { eq as eq5, and as and3, desc as desc2 } from "drizzle-orm";
487
811
  var MetadataEntityRepository = class extends BaseRepository {
488
812
  /**
489
813
  * Bulk upsert with a caller-specified conflict target.
@@ -510,9 +834,9 @@ var MetadataEntityRepository = class extends BaseRepository {
510
834
  */
511
835
  async findByEntityIdAndType(entityId, entityType) {
512
836
  const rows = await this.baseQuery().where(
513
- and(
514
- eq4(this.table["entityId"], entityId),
515
- eq4(this.table["entityType"], entityType)
837
+ and3(
838
+ eq5(this.table["entityId"], entityId),
839
+ eq5(this.table["entityType"], entityType)
516
840
  )
517
841
  );
518
842
  return rows;
@@ -521,14 +845,14 @@ var MetadataEntityRepository = class extends BaseRepository {
521
845
  * List all metadata records for an entity.
522
846
  */
523
847
  async listByEntityId(entityId) {
524
- const rows = await this.baseQuery().where(eq4(this.table["entityId"], entityId));
848
+ const rows = await this.baseQuery().where(eq5(this.table["entityId"], entityId));
525
849
  return rows;
526
850
  }
527
851
  /**
528
852
  * List metadata history for an entity, ordered by validFrom descending.
529
853
  */
530
854
  async listHistoryByEntityId(entityId) {
531
- const rows = await this.baseQuery().where(eq4(this.table["entityId"], entityId)).orderBy(desc2(this.table["validFrom"]));
855
+ const rows = await this.baseQuery().where(eq5(this.table["entityId"], entityId)).orderBy(desc2(this.table["validFrom"]));
532
856
  return rows;
533
857
  }
534
858
  };
@@ -644,6 +968,7 @@ export {
644
968
  BaseListUseCase,
645
969
  BaseRepository,
646
970
  BaseService,
971
+ JunctionSyncRepository,
647
972
  KnowledgeEntityRepository,
648
973
  KnowledgeEntityService,
649
974
  MetadataEntityRepository,
@@ -652,9 +977,11 @@ export {
652
977
  SyncedEntityService,
653
978
  WithAnalytics,
654
979
  buildChangeEvents,
980
+ buildCompositeExternalId,
655
981
  buildLifecycleEvent,
656
982
  diffSnapshots,
657
983
  emitSafely,
658
- entitySnapshot
984
+ entitySnapshot,
985
+ parseCompositeExternalId
659
986
  };
660
987
  //# sourceMappingURL=index.js.map