@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.
- 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 +1 -0
- 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
|
@@ -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:
|
|
85
|
-
query = query.where(
|
|
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
|
-
*
|
|
438
|
-
*
|
|
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
|
|
441
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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(
|
|
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(
|
|
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
|