@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.
- package/dist/runtime/base-classes/index.d.ts +2 -0
- package/dist/runtime/base-classes/index.js +356 -18
- package/dist/runtime/base-classes/index.js.map +1 -1
- package/dist/runtime/base-classes/junction-sync-repository.d.ts +89 -0
- package/dist/runtime/base-classes/junction-sync-repository.js +373 -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 +295 -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,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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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(
|
|
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(
|
|
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
|