@warlock.js/cascade 5.31.0 → 5.33.0
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/CHANGELOG.md +17 -0
- package/cjs/index.cjs +104 -85
- package/cjs/index.cjs.map +1 -1
- package/esm/context/database-data-source-context.d.mts +2 -3
- package/esm/context/database-data-source-context.d.mts.map +1 -1
- package/esm/context/database-transaction-context.d.mts +2 -3
- package/esm/context/database-transaction-context.d.mts.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +56 -36
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/database-id-generator.contract.d.mts +7 -5
- package/esm/contracts/database-id-generator.contract.d.mts.map +1 -1
- package/esm/contracts/database-remover.contract.d.mts +3 -5
- package/esm/contracts/database-remover.contract.d.mts.map +1 -1
- package/esm/contracts/database-restorer.contract.d.mts +6 -6
- package/esm/contracts/database-restorer.contract.d.mts.map +1 -1
- package/esm/contracts/database-writer.contract.d.mts +4 -6
- package/esm/contracts/database-writer.contract.d.mts.map +1 -1
- package/esm/contracts/driver-blueprint.contract.d.mts +2 -3
- package/esm/contracts/driver-blueprint.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/migration-driver.contract.d.mts +101 -53
- package/esm/contracts/migration-driver.contract.d.mts.map +1 -1
- package/esm/contracts/query-builder.contract.d.mts +51 -33
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/contracts/sync-adapter.contract.d.mts +25 -14
- package/esm/contracts/sync-adapter.contract.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.d.mts +4 -5
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source.d.mts +8 -5
- package/esm/data-source/data-source.d.mts.map +1 -1
- package/esm/database-dirty-tracker.d.mts +2 -2
- package/esm/database-dirty-tracker.d.mts.map +1 -1
- package/esm/database-dirty-tracker.mjs +3 -2
- package/esm/database-dirty-tracker.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +3 -4
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts +2 -3
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -3
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +2 -3
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +8 -5
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts +1 -3
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +7 -6
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +2 -6
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts +1 -3
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts.map +1 -1
- package/esm/drivers/mongodb/types.d.mts +11 -9
- package/esm/drivers/mongodb/types.d.mts.map +1 -1
- package/esm/drivers/postgres/index.d.mts +2 -1
- package/esm/drivers/postgres/index.mjs +1 -1
- package/esm/drivers/postgres/naming.mjs +1 -1
- package/esm/drivers/postgres/postgres-blueprint.d.mts +1 -3
- package/esm/drivers/postgres/postgres-blueprint.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts +4 -6
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +4 -5
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.mts +1 -3
- package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.mjs +13 -10
- package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +4 -4
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +14 -10
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +16 -11
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-sql-serializer.mjs +13 -10
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-ttl-purge.mjs +1 -1
- package/esm/drivers/postgres/postgres-type-converters.mjs +2 -2
- package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-update-validator.mjs +1 -1
- package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -1
- package/esm/drivers/postgres/types.d.mts +87 -45
- package/esm/drivers/postgres/types.d.mts.map +1 -1
- package/esm/drivers/sql/sql-dialect.contract.d.mts +1 -3
- package/esm/drivers/sql/sql-dialect.contract.d.mts.map +1 -1
- package/esm/drivers/sql/sql-types.d.mts +113 -59
- package/esm/drivers/sql/sql-types.d.mts.map +1 -1
- package/esm/errors/missing-data-source.error.d.mts +1 -2
- package/esm/errors/missing-data-source.error.d.mts.map +1 -1
- package/esm/errors/transaction-rollback.error.d.mts +1 -2
- package/esm/errors/transaction-rollback.error.d.mts.map +1 -1
- package/esm/errors/undefined-where-value.error.d.mts +1 -2
- package/esm/errors/undefined-where-value.error.d.mts.map +1 -1
- package/esm/errors/unsafe-filter.error.d.mts +1 -2
- package/esm/errors/unsafe-filter.error.d.mts.map +1 -1
- package/esm/errors/unsafe-raw-expression.error.d.mts +1 -2
- package/esm/errors/unsafe-raw-expression.error.d.mts.map +1 -1
- package/esm/errors/unsupported-lean-operation.error.d.mts +1 -2
- package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -1
- package/esm/errors/unsupported-lock-for-update.error.d.mts +1 -2
- package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -1
- package/esm/errors/unsupported-query-operation.error.d.mts +1 -2
- package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -1
- package/esm/errors/unsupported-update-operation.error.d.mts +1 -2
- package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -1
- package/esm/events/model-events.d.mts +8 -6
- package/esm/events/model-events.d.mts.map +1 -1
- package/esm/expressions/aggregate-expressions.d.mts +7 -7
- package/esm/expressions/aggregate-expressions.d.mts.map +1 -1
- package/esm/expressions/column-expressions.d.mts +11 -11
- package/esm/expressions/column-expressions.d.mts.map +1 -1
- package/esm/index.d.mts +7 -2
- package/esm/index.mjs +1 -1
- package/esm/migration/column-builder.d.mts +2 -3
- package/esm/migration/column-builder.d.mts.map +1 -1
- package/esm/migration/column-helpers.d.mts +49 -50
- package/esm/migration/column-helpers.d.mts.map +1 -1
- package/esm/migration/column-helpers.mjs +1 -1
- package/esm/migration/foreign-key-builder.d.mts +2 -3
- package/esm/migration/foreign-key-builder.d.mts.map +1 -1
- package/esm/migration/index.d.mts +3 -2
- package/esm/migration/index.mjs +2 -2
- package/esm/migration/migration-runner.d.mts +7 -6
- package/esm/migration/migration-runner.d.mts.map +1 -1
- package/esm/migration/migration-runner.mjs +15 -11
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/esm/migration/migration.d.mts +50 -32
- package/esm/migration/migration.d.mts.map +1 -1
- package/esm/migration/migration.mjs +1 -3
- package/esm/migration/migration.mjs.map +1 -1
- package/esm/migration/sql-serializer.d.mts +1 -3
- package/esm/migration/sql-serializer.d.mts.map +1 -1
- package/esm/migration/types.d.mts +7 -7
- package/esm/migration/types.d.mts.map +1 -1
- package/esm/model/methods/accessor-methods.mjs +1 -1
- package/esm/model/methods/accessor-methods.mjs.map +1 -1
- package/esm/model/methods/meta-methods.mjs +4 -2
- package/esm/model/methods/meta-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs +1 -1
- package/esm/model/methods/write-methods.d.mts +3 -2
- package/esm/model/methods/write-methods.d.mts.map +1 -1
- package/esm/model/methods/write-methods.mjs +5 -2
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +9 -9
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.types.d.mts +7 -7
- package/esm/model/model.types.d.mts.map +1 -1
- package/esm/model/register-model.d.mts +13 -15
- package/esm/model/register-model.d.mts.map +1 -1
- package/esm/model/relation-decorators.d.mts +6 -8
- package/esm/model/relation-decorators.d.mts.map +1 -1
- package/esm/operations/database.d.mts +13 -8
- package/esm/operations/database.d.mts.map +1 -1
- package/esm/operations/index.d.mts +2 -1
- package/esm/operations/index.mjs +1 -1
- package/esm/operations/migrations.d.mts +18 -14
- package/esm/operations/migrations.d.mts.map +1 -1
- package/esm/query-builder/query-builder.d.mts +2 -4
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs +12 -11
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/relations/index.d.mts +2 -1
- package/esm/relations/index.mjs +1 -1
- package/esm/relations/key-conventions.mjs +2 -1
- package/esm/relations/key-conventions.mjs.map +1 -1
- package/esm/relations/pivot-operations.d.mts +3 -5
- package/esm/relations/pivot-operations.d.mts.map +1 -1
- package/esm/relations/relation-hydrator.d.mts +3 -5
- package/esm/relations/relation-hydrator.d.mts.map +1 -1
- package/esm/relations/relation-loader.d.mts +2 -4
- package/esm/relations/relation-loader.d.mts.map +1 -1
- package/esm/relations/relation-loader.mjs +4 -5
- package/esm/relations/relation-loader.mjs.map +1 -1
- package/esm/relations/types.d.mts +14 -15
- package/esm/relations/types.d.mts.map +1 -1
- package/esm/remover/database-remover.d.mts +1 -3
- package/esm/remover/database-remover.d.mts.map +1 -1
- package/esm/restorer/database-restorer.d.mts +1 -3
- package/esm/restorer/database-restorer.d.mts.map +1 -1
- package/esm/restorer/database-restorer.mjs +2 -1
- package/esm/restorer/database-restorer.mjs.map +1 -1
- package/esm/sql-database-dirty-tracker.d.mts +1 -3
- package/esm/sql-database-dirty-tracker.d.mts.map +1 -1
- package/esm/sync/model-events.mjs +1 -1
- package/esm/sync/model-sync-operation.d.mts +2 -4
- package/esm/sync/model-sync-operation.d.mts.map +1 -1
- package/esm/sync/model-sync.d.mts +3 -4
- package/esm/sync/model-sync.d.mts.map +1 -1
- package/esm/sync/sync-context.d.mts +2 -4
- package/esm/sync/sync-context.d.mts.map +1 -1
- package/esm/sync/sync-fanout.mjs +1 -1
- package/esm/sync/sync-manager.d.mts +2 -4
- package/esm/sync/sync-manager.d.mts.map +1 -1
- package/esm/sync/types.d.mts +121 -64
- package/esm/sync/types.d.mts.map +1 -1
- package/esm/transactions/after-commit.d.mts +2 -2
- package/esm/transactions/after-commit.d.mts.map +1 -1
- package/esm/types.d.mts +7 -8
- package/esm/types.d.mts.map +1 -1
- package/esm/utils/connect-to-database.d.mts +7 -8
- package/esm/utils/connect-to-database.d.mts.map +1 -1
- package/esm/utils/database-writer.utils.d.mts +3 -4
- package/esm/utils/database-writer.utils.d.mts.map +1 -1
- package/esm/utils/define-model.d.mts +3 -5
- package/esm/utils/define-model.d.mts.map +1 -1
- package/esm/utils/escape-regex.d.mts +3 -4
- package/esm/utils/escape-regex.d.mts.map +1 -1
- package/esm/utils/once-connected.d.mts +2 -4
- package/esm/utils/once-connected.d.mts.map +1 -1
- package/esm/utils/sanitize-filter.d.mts +3 -4
- package/esm/utils/sanitize-filter.d.mts.map +1 -1
- package/esm/validation/database-seal-plugins.d.mts +2 -1
- package/esm/validation/database-writer-validation-error.d.mts +1 -3
- package/esm/validation/database-writer-validation-error.d.mts.map +1 -1
- package/esm/validation/index.d.mts +3 -1
- package/esm/validation/index.mjs +1 -1
- package/esm/validation/plugins/database-rules-plugin.d.mts +2 -3
- package/esm/validation/plugins/database-rules-plugin.d.mts.map +1 -1
- package/esm/validation/plugins/embed-validator-plugin.d.mts +4 -3
- package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
- package/esm/validation/types.d.mts +14 -10
- package/esm/validation/types.d.mts.map +1 -1
- package/esm/validation/validators/embed-validator.d.mts +2 -3
- package/esm/validation/validators/embed-validator.d.mts.map +1 -1
- package/esm/writer/database-writer.d.mts +1 -3
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +1 -1
- package/esm/writer/database-writer.mjs.map +1 -1
- package/package.json +5 -5
|
@@ -180,9 +180,7 @@ var RelationLoader = class RelationLoader {
|
|
|
180
180
|
case "belongsTo":
|
|
181
181
|
await this.loadBelongsTo(rootRelation, definition, constraint);
|
|
182
182
|
break;
|
|
183
|
-
case "belongsToMany":
|
|
184
|
-
await this.loadBelongsToMany(rootRelation, definition, constraint);
|
|
185
|
-
break;
|
|
183
|
+
case "belongsToMany": await this.loadBelongsToMany(rootRelation, definition, constraint);
|
|
186
184
|
}
|
|
187
185
|
if (children.length > 0) await this.loadNestedRelations(rootRelation, children, childConstraints);
|
|
188
186
|
}
|
|
@@ -336,7 +334,8 @@ var RelationLoader = class RelationLoader {
|
|
|
336
334
|
if (relatedModels.length === 0) return;
|
|
337
335
|
const parentDefinition = this.getRelationDefinition(parentRelation);
|
|
338
336
|
if (!parentDefinition) return;
|
|
339
|
-
|
|
337
|
+
const RelatedModelClass = this.resolveModelClass(parentDefinition.model);
|
|
338
|
+
await new RelationLoader(relatedModels, RelatedModelClass).load(remainingPaths, constraints);
|
|
340
339
|
}
|
|
341
340
|
/**
|
|
342
341
|
* Parses a dot-notation relation path into segments.
|
|
@@ -372,7 +371,7 @@ var RelationLoader = class RelationLoader {
|
|
|
372
371
|
*/
|
|
373
372
|
applySelect(query, definition, requiredKeys) {
|
|
374
373
|
if (!definition.select || definition.select.length === 0) return;
|
|
375
|
-
query.select([
|
|
374
|
+
query.select([.../* @__PURE__ */ new Set([...definition.select, ...requiredKeys])]);
|
|
376
375
|
}
|
|
377
376
|
/**
|
|
378
377
|
* Gets the relation definition from the model class.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relation-loader.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/relation-loader.ts"],"sourcesContent":["/**\n * @fileoverview Core relation loading logic for the Cascade ORM.\n *\n * The RelationLoader is responsible for efficiently loading related models\n * while preventing N+1 query problems through batch loading strategies.\n *\n * @module @warlock.js/cascade/relations/relation-loader\n */\n\nimport { isLazy, type Lazy } from \"@mongez/reinforcements\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelFromRegistry, resolveModelName } from \"../model/register-model\";\nimport {\n inferBelongsToForeignKey,\n inferHasForeignKey,\n inferPivotKey,\n inferPivotTable,\n} from \"./key-conventions\";\nimport type {\n LoadedRelationResult,\n RelationConstraintCallback,\n RelationConstraints,\n RelationDefinition,\n} from \"./types\";\n\n// ============================================================================\n// SHARED HELPER — SINGLE SOURCE OF TRUTH FOR LOADED RELATIONS\n// ============================================================================\n\n/**\n * Attach a loaded relation onto a model instance, keeping the\n * `loadedRelations` Map and the direct property access in sync.\n *\n * The property is installed as a `defineProperty` getter/setter façade over\n * the Map — `model.posts` reads from `loadedRelations.get(\"posts\")`, and\n * assigning `model.posts = newPosts` writes back to the Map. Eliminates the\n * historical drift between the two storage slots: any mutation visible via\n * one path is visible via the other.\n *\n * Used by both `RelationLoader` (the `with()` path) and the Postgres\n * driver's `attachJoinedRelations` (the `joinWith()` path).\n *\n * @example\n * attachLoadedRelation(user, \"posts\", postsArray);\n * user.posts === user.loadedRelations.get(\"posts\"); // always true\n */\nexport function attachLoadedRelation(\n model: object,\n name: string,\n value: LoadedRelationResult,\n): void {\n const modelWithRelations = model as {\n loadedRelations?: Map<string, LoadedRelationResult>;\n };\n\n if (!modelWithRelations.loadedRelations) {\n modelWithRelations.loadedRelations = new Map();\n }\n\n const relations = modelWithRelations.loadedRelations;\n relations.set(name, value);\n\n // Install a getter/setter on the instance so direct property access reads\n // from (and writes to) the Map. `configurable: true` lets a later reload\n // re-define the property; `enumerable: true` so JSON.stringify and\n // Object.keys see the relation.\n Object.defineProperty(model, name, {\n configurable: true,\n enumerable: true,\n get(): LoadedRelationResult | undefined {\n return relations.get(name);\n },\n set(next: LoadedRelationResult): void {\n relations.set(name, next);\n },\n });\n}\n\n// ============================================================================\n// RELATION LOADER CLASS\n// ============================================================================\n\n/**\n * Efficiently loads relationships for one or more model instances.\n *\n * The RelationLoader handles:\n * - Batch loading to prevent N+1 query problems\n * - Nested relation loading via dot notation\n * - Constrained loading with query callbacks\n * - All relation types (hasOne, hasMany, belongsTo, belongsToMany)\n *\n * @template TModel - The type of model being loaded\n *\n * @example\n * ```typescript\n * const users = await User.query().get();\n * const loader = new RelationLoader(users, User);\n *\n * // Load single relation\n * await loader.load(\"posts\");\n *\n * // Load multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // Load nested relations\n * await loader.load(\"posts.comments.author\");\n *\n * // Load with constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"isPublished\", true),\n * });\n * ```\n */\nexport class RelationLoader<TModel extends Model = Model> {\n // ==========================================================================\n // PRIVATE PROPERTIES\n // ==========================================================================\n\n /**\n * The model instances to load relations for.\n */\n private readonly models: TModel[];\n\n /**\n * The model class constructor.\n */\n private readonly modelClass: ChildModel<TModel>;\n\n // ==========================================================================\n // CONSTRUCTOR\n // ==========================================================================\n\n /**\n * Creates a new RelationLoader instance.\n *\n * @param models - The model instances to load relations for\n * @param modelClass - The model class constructor\n */\n public constructor(models: TModel[], modelClass: ChildModel<TModel>) {\n this.models = models;\n this.modelClass = modelClass;\n }\n\n /**\n * Read the configured relation conventions from this model's data\n * source. Returns `undefined` when no overrides are set — the inference\n * helpers fall back to framework defaults in that case.\n */\n private get relationDefaults() {\n try {\n const dataSource = this.modelClass.getDataSource();\n\n if (!dataSource) return undefined;\n\n const namingConvention =\n dataSource.modelDefaults?.namingConvention ??\n dataSource.driver?.modelDefaults?.namingConvention;\n\n return { ...dataSource.relationDefaults, namingConvention };\n } catch {\n return undefined;\n }\n }\n\n // ==========================================================================\n // PUBLIC METHODS\n // ==========================================================================\n\n /**\n * Loads one or more relations for all model instances.\n *\n * @param relations - A single relation name, array of names, or constraints object\n * @param constraints - Optional constraints to apply to the loading query\n *\n * @example\n * ```typescript\n * // Single relation\n * await loader.load(\"posts\");\n *\n * // Multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // With constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"status\", \"active\"),\n * });\n * ```\n */\n public async load(\n relations: string | string[],\n constraints?: RelationConstraints,\n ): Promise<void> {\n // If no models, nothing to load\n if (this.models.length === 0) {\n return;\n }\n\n // Normalize to array\n const relationNames = Array.isArray(relations) ? relations : [relations];\n\n // Group the paths into a tree keyed by root relation, so a shared prefix\n // (\"posts.comments\", \"posts.tags\") is loaded ONCE and every child path is\n // loaded onto those same instances instead of overwriting them.\n const nodes = new Map<\n string,\n {\n constraint?: RelationConstraintCallback;\n children: string[];\n childConstraints: RelationConstraints;\n }\n >();\n\n for (const relationName of relationNames) {\n const [root, ...rest] = this.parseNestedRelation(relationName);\n const key = root ?? relationName;\n\n if (!nodes.has(key)) {\n nodes.set(key, { children: [], childConstraints: {} });\n }\n\n const node = nodes.get(key)!;\n const constraint = constraints?.[relationName];\n const callbackConstraint = typeof constraint === \"function\" ? constraint : undefined;\n\n if (rest.length === 0) {\n // A constraint belongs to the LAST segment of its path only.\n if (callbackConstraint) node.constraint = callbackConstraint;\n continue;\n }\n\n const childPath = rest.join(\".\");\n\n if (!node.children.includes(childPath)) {\n node.children.push(childPath);\n }\n\n if (callbackConstraint) node.childConstraints[childPath] = callbackConstraint;\n }\n\n for (const [root, node] of nodes) {\n await this.loadRelation(root, node.constraint, node.children, node.childConstraints);\n }\n }\n\n // ==========================================================================\n // PRIVATE METHODS - RELATION LOADING\n // ==========================================================================\n\n /**\n * Loads a single (root) relation once, then its child paths onto the same\n * loaded instances.\n *\n * @param name - The root relation name\n * @param constraint - Optional constraint callback, applied to this relation only\n * @param children - Remaining dot paths below this relation\n * @param childConstraints - Constraints keyed by the remaining dot paths\n */\n private async loadRelation(\n name: string,\n constraint?: RelationConstraintCallback,\n children: string[] = [],\n childConstraints: RelationConstraints = {},\n ): Promise<void> {\n const rootRelation: string | undefined = name || undefined;\n\n // An empty parse would otherwise reach `getRelationDefinition(undefined)`\n // and fail as `Relation \"undefined\" is not defined on model \"X\"` — a\n // message that sends the reader looking for a relation they never wrote,\n // instead of at the name they passed. Same failure, named correctly.\n if (rootRelation === undefined) {\n throw new Error(\n `Relation name \"${name}\" could not be parsed into a relation path on model ` +\n `\"${this.modelClass.name}\".`,\n );\n }\n\n // Get the relation definition from the model class\n const definition = this.getRelationDefinition(rootRelation);\n\n if (!definition) {\n throw new Error(\n `Relation \"${rootRelation}\" is not defined on model \"${this.modelClass.name}\". ` +\n `Make sure to define it in the static 'relations' property.`,\n );\n }\n\n // Load based on relation type\n switch (definition.type) {\n case \"hasMany\":\n await this.loadHasMany(rootRelation, definition, constraint);\n break;\n\n case \"hasOne\":\n await this.loadHasOne(rootRelation, definition, constraint);\n break;\n\n case \"belongsTo\":\n await this.loadBelongsTo(rootRelation, definition, constraint);\n break;\n\n case \"belongsToMany\":\n await this.loadBelongsToMany(rootRelation, definition, constraint);\n break;\n }\n\n // If there are nested relations, load them recursively\n if (children.length > 0) {\n await this.loadNestedRelations(rootRelation, children, childConstraints);\n }\n }\n\n /**\n * Loads a hasMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n // No values to query, set empty arrays\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Group by foreign key and assign to models\n const recordsByForeignKey = this.groupBy(relatedRecords as Model[], foreignKey);\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? [];\n });\n }\n\n /**\n * Loads a hasOne relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasOne(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by foreign key\n const recordsByForeignKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const fkValue = record.get(foreignKey);\n // For hasOne, take the first match only\n if (!recordsByForeignKey.has(fkValue)) {\n recordsByForeignKey.set(fkValue, record);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsTo relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsTo(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const foreignKey =\n definition.foreignKey ?? inferBelongsToForeignKey(name, this.relationDefaults);\n const ownerKey = definition.localKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all foreign key values from the models\n const foreignKeyValues = this.collectKeyValues(foreignKey);\n\n if (foreignKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(ownerKey, foreignKeyValues);\n\n this.applySelect(query, definition, [ownerKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by owner key\n const recordsByOwnerKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const keyValue = record.get(ownerKey);\n recordsByOwnerKey.set(keyValue, record);\n }\n\n this.setRelationOnModels(name, (model) => {\n const fkValue = model.get(foreignKey);\n return recordsByOwnerKey.get(fkValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsToMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsToMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const relatedModelName = resolveModelName(definition.model);\n const pivotTable =\n definition.pivot ??\n inferPivotTable(this.modelClass.name, relatedModelName, this.relationDefaults);\n const localKey = definition.pivotLocalKey ?? this.modelClass.primaryKey ?? \"id\";\n const pivotLocalKey =\n definition.localKey ?? inferPivotKey(this.modelClass.name, this.relationDefaults);\n const pivotForeignKey =\n definition.foreignKey ?? inferPivotKey(relatedModelName, this.relationDefaults);\n const relatedKey = definition.pivotForeignKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 1: Query the pivot table to get the relationships\n const dataSource = this.modelClass.getDataSource();\n const pivotQuery = dataSource.driver\n .queryBuilder(pivotTable)\n .whereIn(pivotLocalKey, localKeyValues);\n\n const pivotRecords = (await pivotQuery.get()) as Record<string, unknown>[];\n\n if (pivotRecords.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 2: Collect related model IDs from pivot\n const relatedIds = [...new Set(pivotRecords.map((p) => p[pivotForeignKey]))];\n\n // Step 3: Query the related model\n const relatedQuery = RelatedModel.query().whereIn(relatedKey, relatedIds);\n\n this.applySelect(relatedQuery, definition, [relatedKey]);\n\n if (constraint) {\n constraint(relatedQuery);\n }\n\n const relatedRecords = await relatedQuery.get();\n\n // Step 4: Index related records by their key\n const relatedById = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n relatedById.set(record.get(relatedKey), record);\n }\n\n // Step 5: Build the relationship map from pivot data\n const relationshipMap = new Map<unknown, Model[]>();\n\n for (const pivot of pivotRecords) {\n const localValue = pivot[pivotLocalKey];\n const foreignValue = pivot[pivotForeignKey];\n const relatedRecord = relatedById.get(foreignValue);\n\n if (relatedRecord) {\n if (!relationshipMap.has(localValue)) {\n relationshipMap.set(localValue, []);\n }\n relationshipMap.get(localValue)!.push(relatedRecord);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return relationshipMap.get(keyValue) ?? [];\n });\n }\n\n // ==========================================================================\n // PRIVATE METHODS - NESTED RELATIONS\n // ==========================================================================\n\n /**\n * Loads nested relations recursively.\n *\n * @param parentRelation - The name of the parent relation\n * @param remainingPaths - The remaining dot paths to load below the parent\n * @param constraints - Constraints keyed by those remaining paths\n */\n private async loadNestedRelations(\n parentRelation: string,\n remainingPaths: string[],\n constraints: RelationConstraints,\n ): Promise<void> {\n // Collect all loaded related models from the parent relation\n const relatedModels: Model[] = [];\n\n for (const model of this.models) {\n const loaded = this.getLoadedRelation(model, parentRelation);\n\n if (Array.isArray(loaded)) {\n relatedModels.push(...loaded);\n } else if (loaded) {\n relatedModels.push(loaded);\n }\n }\n\n if (relatedModels.length === 0) {\n return;\n }\n\n // Get the related model class\n const parentDefinition = this.getRelationDefinition(parentRelation);\n if (!parentDefinition) return;\n\n const RelatedModelClass = this.resolveModelClass(parentDefinition.model);\n\n // Create a new loader for the nested relation\n const nestedLoader = new RelationLoader(relatedModels, RelatedModelClass as ChildModel<Model>);\n\n // Load the next level\n await nestedLoader.load(remainingPaths, constraints);\n }\n\n // ==========================================================================\n // PRIVATE METHODS - UTILITIES\n // ==========================================================================\n\n /**\n * Parses a dot-notation relation path into segments.\n *\n * @param path - The relation path (e.g., \"posts.comments.author\")\n * @returns An array of relation names\n */\n private parseNestedRelation(path: string): string[] {\n return path.split(\".\");\n }\n\n /**\n * Resolves a model class from the registry by name.\n *\n * @param name - The registered model name\n * @returns The model class constructor\n * @throws Error if the model is not found in the registry\n */\n private resolveModelClass(model: string | ChildModel<Model> | Lazy<unknown>): ChildModel<Model> {\n if (typeof model === \"function\") return model;\n if (isLazy(model)) return model.resolve() as ChildModel<Model>;\n\n const ModelClass = getModelFromRegistry(model);\n\n if (!ModelClass) {\n const callerName = this.modelClass.name || \"unknown\";\n throw new Error(\n `Cannot resolve relation target — model \"${model}\" is not registered.\\n` +\n ` Caller: ${callerName} (relation target reference)\\n` +\n ` Common causes:\\n` +\n ` - The target model is missing the @RegisterModel() decorator\\n` +\n ` - The target model's module is not imported anywhere at startup\\n` +\n ` - Circular import between the caller and target — one of them sees\\n` +\n ` the other as undefined during module load\\n` +\n ` Fix: add an explicit \\`import \"<path-to-${model}-model>\";\\` to your app's\\n` +\n ` entry point so the decorator runs before any query consults this relation.\\n` +\n ` Alternative: declare the relation with \\`lazy(() => ${model})\\` instead of a string\\n` +\n ` to bind directly to the class (no registry lookup needed).`,\n );\n }\n\n return ModelClass;\n }\n\n /**\n * Applies the relation definition's `select` columns to the related query.\n * Runs before the constraint callback so an explicit constraint can still\n * override it. The key columns used to match records back to their owners\n * are always included.\n */\n private applySelect(\n query: { select: (columns: string[]) => unknown },\n definition: RelationDefinition,\n requiredKeys: string[],\n ): void {\n if (!definition.select || definition.select.length === 0) return;\n\n query.select([...new Set([...definition.select, ...requiredKeys])]);\n }\n\n /**\n * Gets the relation definition from the model class.\n *\n * @param name - The relation name\n * @returns The relation definition or undefined\n */\n private getRelationDefinition(name: string): RelationDefinition | undefined {\n const relations = (\n this.modelClass as unknown as { relations?: Record<string, RelationDefinition> }\n ).relations;\n return relations?.[name];\n }\n\n /**\n * Collects unique key values from all models.\n *\n * @param key - The key to collect values for\n * @returns An array of unique key values\n */\n private collectKeyValues(key: string): unknown[] {\n const values = new Set<unknown>();\n\n for (const model of this.models) {\n const value = model.get(key);\n if (value !== undefined && value !== null) {\n values.add(value);\n }\n }\n\n return [...values];\n }\n\n /**\n * Groups records by a key value.\n *\n * @param records - The records to group\n * @param key - The key to group by\n * @returns A map of key values to records\n */\n private groupBy(records: Model[], key: string): Map<unknown, Model[]> {\n const groups = new Map<unknown, Model[]>();\n\n for (const record of records) {\n const keyValue = record.get(key);\n\n if (!groups.has(keyValue)) {\n groups.set(keyValue, []);\n }\n\n groups.get(keyValue)!.push(record);\n }\n\n return groups;\n }\n\n /**\n * Sets a relation value on all models using a getter function.\n *\n * @param name - The relation name\n * @param getter - Function that returns the relation value for each model\n */\n private setRelationOnModels(name: string, getter: (model: TModel) => LoadedRelationResult): void {\n for (const model of this.models) {\n const value = getter(model);\n this.setLoadedRelation(model, name, value);\n }\n }\n\n /**\n * Gets a loaded relation from a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @returns The loaded relation value or undefined\n */\n private getLoadedRelation(model: TModel, name: string): LoadedRelationResult | undefined {\n const loadedRelations = (\n model as unknown as { loadedRelations?: Map<string, LoadedRelationResult> }\n ).loadedRelations;\n return loadedRelations?.get(name);\n }\n\n /**\n * Sets a loaded relation on a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @param value - The relation value\n */\n private setLoadedRelation(model: TModel, name: string, value: LoadedRelationResult): void {\n attachLoadedRelation(model as object, name, value);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"relation-loader.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/relation-loader.ts"],"sourcesContent":["/**\n * @fileoverview Core relation loading logic for the Cascade ORM.\n *\n * The RelationLoader is responsible for efficiently loading related models\n * while preventing N+1 query problems through batch loading strategies.\n *\n * @module @warlock.js/cascade/relations/relation-loader\n */\n\nimport { isLazy, type Lazy } from \"@mongez/reinforcements\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelFromRegistry, resolveModelName } from \"../model/register-model\";\nimport {\n inferBelongsToForeignKey,\n inferHasForeignKey,\n inferPivotKey,\n inferPivotTable,\n} from \"./key-conventions\";\nimport type {\n LoadedRelationResult,\n RelationConstraintCallback,\n RelationConstraints,\n RelationDefinition,\n} from \"./types\";\n\n// ============================================================================\n// SHARED HELPER — SINGLE SOURCE OF TRUTH FOR LOADED RELATIONS\n// ============================================================================\n\n/**\n * Attach a loaded relation onto a model instance, keeping the\n * `loadedRelations` Map and the direct property access in sync.\n *\n * The property is installed as a `defineProperty` getter/setter façade over\n * the Map — `model.posts` reads from `loadedRelations.get(\"posts\")`, and\n * assigning `model.posts = newPosts` writes back to the Map. Eliminates the\n * historical drift between the two storage slots: any mutation visible via\n * one path is visible via the other.\n *\n * Used by both `RelationLoader` (the `with()` path) and the Postgres\n * driver's `attachJoinedRelations` (the `joinWith()` path).\n *\n * @example\n * attachLoadedRelation(user, \"posts\", postsArray);\n * user.posts === user.loadedRelations.get(\"posts\"); // always true\n */\nexport function attachLoadedRelation(\n model: object,\n name: string,\n value: LoadedRelationResult,\n): void {\n const modelWithRelations = model as {\n loadedRelations?: Map<string, LoadedRelationResult>;\n };\n\n if (!modelWithRelations.loadedRelations) {\n modelWithRelations.loadedRelations = new Map();\n }\n\n const relations = modelWithRelations.loadedRelations;\n relations.set(name, value);\n\n // Install a getter/setter on the instance so direct property access reads\n // from (and writes to) the Map. `configurable: true` lets a later reload\n // re-define the property; `enumerable: true` so JSON.stringify and\n // Object.keys see the relation.\n Object.defineProperty(model, name, {\n configurable: true,\n enumerable: true,\n get(): LoadedRelationResult | undefined {\n return relations.get(name);\n },\n set(next: LoadedRelationResult): void {\n relations.set(name, next);\n },\n });\n}\n\n// ============================================================================\n// RELATION LOADER CLASS\n// ============================================================================\n\n/**\n * Efficiently loads relationships for one or more model instances.\n *\n * The RelationLoader handles:\n * - Batch loading to prevent N+1 query problems\n * - Nested relation loading via dot notation\n * - Constrained loading with query callbacks\n * - All relation types (hasOne, hasMany, belongsTo, belongsToMany)\n *\n * @template TModel - The type of model being loaded\n *\n * @example\n * ```typescript\n * const users = await User.query().get();\n * const loader = new RelationLoader(users, User);\n *\n * // Load single relation\n * await loader.load(\"posts\");\n *\n * // Load multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // Load nested relations\n * await loader.load(\"posts.comments.author\");\n *\n * // Load with constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"isPublished\", true),\n * });\n * ```\n */\nexport class RelationLoader<TModel extends Model = Model> {\n // ==========================================================================\n // PRIVATE PROPERTIES\n // ==========================================================================\n\n /**\n * The model instances to load relations for.\n */\n private readonly models: TModel[];\n\n /**\n * The model class constructor.\n */\n private readonly modelClass: ChildModel<TModel>;\n\n // ==========================================================================\n // CONSTRUCTOR\n // ==========================================================================\n\n /**\n * Creates a new RelationLoader instance.\n *\n * @param models - The model instances to load relations for\n * @param modelClass - The model class constructor\n */\n public constructor(models: TModel[], modelClass: ChildModel<TModel>) {\n this.models = models;\n this.modelClass = modelClass;\n }\n\n /**\n * Read the configured relation conventions from this model's data\n * source. Returns `undefined` when no overrides are set — the inference\n * helpers fall back to framework defaults in that case.\n */\n private get relationDefaults() {\n try {\n const dataSource = this.modelClass.getDataSource();\n\n if (!dataSource) return undefined;\n\n const namingConvention =\n dataSource.modelDefaults?.namingConvention ??\n dataSource.driver?.modelDefaults?.namingConvention;\n\n return { ...dataSource.relationDefaults, namingConvention };\n } catch {\n return undefined;\n }\n }\n\n // ==========================================================================\n // PUBLIC METHODS\n // ==========================================================================\n\n /**\n * Loads one or more relations for all model instances.\n *\n * @param relations - A single relation name, array of names, or constraints object\n * @param constraints - Optional constraints to apply to the loading query\n *\n * @example\n * ```typescript\n * // Single relation\n * await loader.load(\"posts\");\n *\n * // Multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // With constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"status\", \"active\"),\n * });\n * ```\n */\n public async load(\n relations: string | string[],\n constraints?: RelationConstraints,\n ): Promise<void> {\n // If no models, nothing to load\n if (this.models.length === 0) {\n return;\n }\n\n // Normalize to array\n const relationNames = Array.isArray(relations) ? relations : [relations];\n\n // Group the paths into a tree keyed by root relation, so a shared prefix\n // (\"posts.comments\", \"posts.tags\") is loaded ONCE and every child path is\n // loaded onto those same instances instead of overwriting them.\n const nodes = new Map<\n string,\n {\n constraint?: RelationConstraintCallback;\n children: string[];\n childConstraints: RelationConstraints;\n }\n >();\n\n for (const relationName of relationNames) {\n const [root, ...rest] = this.parseNestedRelation(relationName);\n const key = root ?? relationName;\n\n if (!nodes.has(key)) {\n nodes.set(key, { children: [], childConstraints: {} });\n }\n\n const node = nodes.get(key)!;\n const constraint = constraints?.[relationName];\n const callbackConstraint = typeof constraint === \"function\" ? constraint : undefined;\n\n if (rest.length === 0) {\n // A constraint belongs to the LAST segment of its path only.\n if (callbackConstraint) node.constraint = callbackConstraint;\n continue;\n }\n\n const childPath = rest.join(\".\");\n\n if (!node.children.includes(childPath)) {\n node.children.push(childPath);\n }\n\n if (callbackConstraint) node.childConstraints[childPath] = callbackConstraint;\n }\n\n for (const [root, node] of nodes) {\n await this.loadRelation(root, node.constraint, node.children, node.childConstraints);\n }\n }\n\n // ==========================================================================\n // PRIVATE METHODS - RELATION LOADING\n // ==========================================================================\n\n /**\n * Loads a single (root) relation once, then its child paths onto the same\n * loaded instances.\n *\n * @param name - The root relation name\n * @param constraint - Optional constraint callback, applied to this relation only\n * @param children - Remaining dot paths below this relation\n * @param childConstraints - Constraints keyed by the remaining dot paths\n */\n private async loadRelation(\n name: string,\n constraint?: RelationConstraintCallback,\n children: string[] = [],\n childConstraints: RelationConstraints = {},\n ): Promise<void> {\n const rootRelation: string | undefined = name || undefined;\n\n // An empty parse would otherwise reach `getRelationDefinition(undefined)`\n // and fail as `Relation \"undefined\" is not defined on model \"X\"` — a\n // message that sends the reader looking for a relation they never wrote,\n // instead of at the name they passed. Same failure, named correctly.\n if (rootRelation === undefined) {\n throw new Error(\n `Relation name \"${name}\" could not be parsed into a relation path on model ` +\n `\"${this.modelClass.name}\".`,\n );\n }\n\n // Get the relation definition from the model class\n const definition = this.getRelationDefinition(rootRelation);\n\n if (!definition) {\n throw new Error(\n `Relation \"${rootRelation}\" is not defined on model \"${this.modelClass.name}\". ` +\n `Make sure to define it in the static 'relations' property.`,\n );\n }\n\n // Load based on relation type\n switch (definition.type) {\n case \"hasMany\":\n await this.loadHasMany(rootRelation, definition, constraint);\n break;\n\n case \"hasOne\":\n await this.loadHasOne(rootRelation, definition, constraint);\n break;\n\n case \"belongsTo\":\n await this.loadBelongsTo(rootRelation, definition, constraint);\n break;\n\n case \"belongsToMany\":\n await this.loadBelongsToMany(rootRelation, definition, constraint);\n break;\n }\n\n // If there are nested relations, load them recursively\n if (children.length > 0) {\n await this.loadNestedRelations(rootRelation, children, childConstraints);\n }\n }\n\n /**\n * Loads a hasMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n // No values to query, set empty arrays\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Group by foreign key and assign to models\n const recordsByForeignKey = this.groupBy(relatedRecords as Model[], foreignKey);\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? [];\n });\n }\n\n /**\n * Loads a hasOne relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasOne(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by foreign key\n const recordsByForeignKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const fkValue = record.get(foreignKey);\n // For hasOne, take the first match only\n if (!recordsByForeignKey.has(fkValue)) {\n recordsByForeignKey.set(fkValue, record);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsTo relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsTo(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const foreignKey =\n definition.foreignKey ?? inferBelongsToForeignKey(name, this.relationDefaults);\n const ownerKey = definition.localKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all foreign key values from the models\n const foreignKeyValues = this.collectKeyValues(foreignKey);\n\n if (foreignKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(ownerKey, foreignKeyValues);\n\n this.applySelect(query, definition, [ownerKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by owner key\n const recordsByOwnerKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const keyValue = record.get(ownerKey);\n recordsByOwnerKey.set(keyValue, record);\n }\n\n this.setRelationOnModels(name, (model) => {\n const fkValue = model.get(foreignKey);\n return recordsByOwnerKey.get(fkValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsToMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsToMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const relatedModelName = resolveModelName(definition.model);\n const pivotTable =\n definition.pivot ??\n inferPivotTable(this.modelClass.name, relatedModelName, this.relationDefaults);\n const localKey = definition.pivotLocalKey ?? this.modelClass.primaryKey ?? \"id\";\n const pivotLocalKey =\n definition.localKey ?? inferPivotKey(this.modelClass.name, this.relationDefaults);\n const pivotForeignKey =\n definition.foreignKey ?? inferPivotKey(relatedModelName, this.relationDefaults);\n const relatedKey = definition.pivotForeignKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 1: Query the pivot table to get the relationships\n const dataSource = this.modelClass.getDataSource();\n const pivotQuery = dataSource.driver\n .queryBuilder(pivotTable)\n .whereIn(pivotLocalKey, localKeyValues);\n\n const pivotRecords = (await pivotQuery.get()) as Record<string, unknown>[];\n\n if (pivotRecords.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 2: Collect related model IDs from pivot\n const relatedIds = [...new Set(pivotRecords.map((p) => p[pivotForeignKey]))];\n\n // Step 3: Query the related model\n const relatedQuery = RelatedModel.query().whereIn(relatedKey, relatedIds);\n\n this.applySelect(relatedQuery, definition, [relatedKey]);\n\n if (constraint) {\n constraint(relatedQuery);\n }\n\n const relatedRecords = await relatedQuery.get();\n\n // Step 4: Index related records by their key\n const relatedById = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n relatedById.set(record.get(relatedKey), record);\n }\n\n // Step 5: Build the relationship map from pivot data\n const relationshipMap = new Map<unknown, Model[]>();\n\n for (const pivot of pivotRecords) {\n const localValue = pivot[pivotLocalKey];\n const foreignValue = pivot[pivotForeignKey];\n const relatedRecord = relatedById.get(foreignValue);\n\n if (relatedRecord) {\n if (!relationshipMap.has(localValue)) {\n relationshipMap.set(localValue, []);\n }\n relationshipMap.get(localValue)!.push(relatedRecord);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return relationshipMap.get(keyValue) ?? [];\n });\n }\n\n // ==========================================================================\n // PRIVATE METHODS - NESTED RELATIONS\n // ==========================================================================\n\n /**\n * Loads nested relations recursively.\n *\n * @param parentRelation - The name of the parent relation\n * @param remainingPaths - The remaining dot paths to load below the parent\n * @param constraints - Constraints keyed by those remaining paths\n */\n private async loadNestedRelations(\n parentRelation: string,\n remainingPaths: string[],\n constraints: RelationConstraints,\n ): Promise<void> {\n // Collect all loaded related models from the parent relation\n const relatedModels: Model[] = [];\n\n for (const model of this.models) {\n const loaded = this.getLoadedRelation(model, parentRelation);\n\n if (Array.isArray(loaded)) {\n relatedModels.push(...loaded);\n } else if (loaded) {\n relatedModels.push(loaded);\n }\n }\n\n if (relatedModels.length === 0) {\n return;\n }\n\n // Get the related model class\n const parentDefinition = this.getRelationDefinition(parentRelation);\n if (!parentDefinition) return;\n\n const RelatedModelClass = this.resolveModelClass(parentDefinition.model);\n\n // Create a new loader for the nested relation\n const nestedLoader = new RelationLoader(relatedModels, RelatedModelClass as ChildModel<Model>);\n\n // Load the next level\n await nestedLoader.load(remainingPaths, constraints);\n }\n\n // ==========================================================================\n // PRIVATE METHODS - UTILITIES\n // ==========================================================================\n\n /**\n * Parses a dot-notation relation path into segments.\n *\n * @param path - The relation path (e.g., \"posts.comments.author\")\n * @returns An array of relation names\n */\n private parseNestedRelation(path: string): string[] {\n return path.split(\".\");\n }\n\n /**\n * Resolves a model class from the registry by name.\n *\n * @param name - The registered model name\n * @returns The model class constructor\n * @throws Error if the model is not found in the registry\n */\n private resolveModelClass(model: string | ChildModel<Model> | Lazy<unknown>): ChildModel<Model> {\n if (typeof model === \"function\") return model;\n if (isLazy(model)) return model.resolve() as ChildModel<Model>;\n\n const ModelClass = getModelFromRegistry(model);\n\n if (!ModelClass) {\n const callerName = this.modelClass.name || \"unknown\";\n throw new Error(\n `Cannot resolve relation target — model \"${model}\" is not registered.\\n` +\n ` Caller: ${callerName} (relation target reference)\\n` +\n ` Common causes:\\n` +\n ` - The target model is missing the @RegisterModel() decorator\\n` +\n ` - The target model's module is not imported anywhere at startup\\n` +\n ` - Circular import between the caller and target — one of them sees\\n` +\n ` the other as undefined during module load\\n` +\n ` Fix: add an explicit \\`import \"<path-to-${model}-model>\";\\` to your app's\\n` +\n ` entry point so the decorator runs before any query consults this relation.\\n` +\n ` Alternative: declare the relation with \\`lazy(() => ${model})\\` instead of a string\\n` +\n ` to bind directly to the class (no registry lookup needed).`,\n );\n }\n\n return ModelClass;\n }\n\n /**\n * Applies the relation definition's `select` columns to the related query.\n * Runs before the constraint callback so an explicit constraint can still\n * override it. The key columns used to match records back to their owners\n * are always included.\n */\n private applySelect(\n query: { select: (columns: string[]) => unknown },\n definition: RelationDefinition,\n requiredKeys: string[],\n ): void {\n if (!definition.select || definition.select.length === 0) return;\n\n query.select([...new Set([...definition.select, ...requiredKeys])]);\n }\n\n /**\n * Gets the relation definition from the model class.\n *\n * @param name - The relation name\n * @returns The relation definition or undefined\n */\n private getRelationDefinition(name: string): RelationDefinition | undefined {\n const relations = (\n this.modelClass as unknown as { relations?: Record<string, RelationDefinition> }\n ).relations;\n return relations?.[name];\n }\n\n /**\n * Collects unique key values from all models.\n *\n * @param key - The key to collect values for\n * @returns An array of unique key values\n */\n private collectKeyValues(key: string): unknown[] {\n const values = new Set<unknown>();\n\n for (const model of this.models) {\n const value = model.get(key);\n if (value !== undefined && value !== null) {\n values.add(value);\n }\n }\n\n return [...values];\n }\n\n /**\n * Groups records by a key value.\n *\n * @param records - The records to group\n * @param key - The key to group by\n * @returns A map of key values to records\n */\n private groupBy(records: Model[], key: string): Map<unknown, Model[]> {\n const groups = new Map<unknown, Model[]>();\n\n for (const record of records) {\n const keyValue = record.get(key);\n\n if (!groups.has(keyValue)) {\n groups.set(keyValue, []);\n }\n\n groups.get(keyValue)!.push(record);\n }\n\n return groups;\n }\n\n /**\n * Sets a relation value on all models using a getter function.\n *\n * @param name - The relation name\n * @param getter - Function that returns the relation value for each model\n */\n private setRelationOnModels(name: string, getter: (model: TModel) => LoadedRelationResult): void {\n for (const model of this.models) {\n const value = getter(model);\n this.setLoadedRelation(model, name, value);\n }\n }\n\n /**\n * Gets a loaded relation from a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @returns The loaded relation value or undefined\n */\n private getLoadedRelation(model: TModel, name: string): LoadedRelationResult | undefined {\n const loadedRelations = (\n model as unknown as { loadedRelations?: Map<string, LoadedRelationResult> }\n ).loadedRelations;\n return loadedRelations?.get(name);\n }\n\n /**\n * Sets a loaded relation on a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @param value - The relation value\n */\n private setLoadedRelation(model: TModel, name: string, value: LoadedRelationResult): void {\n attachLoadedRelation(model as object, name, value);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ChildModel } from "../model/model.types.mjs";
|
|
2
2
|
import { Model } from "../model/model.mjs";
|
|
3
3
|
import { QueryBuilderContract } from "../contracts/query-builder.contract.mjs";
|
|
4
|
+
import "../contracts/index.mjs";
|
|
4
5
|
import { Lazy } from "@mongez/reinforcements";
|
|
5
|
-
|
|
6
6
|
//#region ../../packages/cascade/src/relations/types.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The type of relationship between models.
|
|
@@ -20,7 +20,7 @@ import { Lazy } from "@mongez/reinforcements";
|
|
|
20
20
|
* // Post belongsToMany Tags (through post_tags pivot table)
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
type RelationType = "hasOne" | "hasMany" | "belongsTo" | "belongsToMany";
|
|
23
|
+
export type RelationType = "hasOne" | "hasMany" | "belongsTo" | "belongsToMany";
|
|
24
24
|
/**
|
|
25
25
|
* Complete definition of a model relationship.
|
|
26
26
|
*
|
|
@@ -37,7 +37,7 @@ type RelationType = "hasOne" | "hasMany" | "belongsTo" | "belongsToMany";
|
|
|
37
37
|
* };
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
|
-
type RelationDefinition = {
|
|
40
|
+
export type RelationDefinition = {
|
|
41
41
|
/**
|
|
42
42
|
* The type of relationship.
|
|
43
43
|
*/
|
|
@@ -103,7 +103,7 @@ type RelationDefinition = {
|
|
|
103
103
|
* };
|
|
104
104
|
* ```
|
|
105
105
|
*/
|
|
106
|
-
type HasManyOptions = {
|
|
106
|
+
export type HasManyOptions = {
|
|
107
107
|
/**
|
|
108
108
|
* The foreign key field on the related model.
|
|
109
109
|
*
|
|
@@ -134,7 +134,7 @@ type HasManyOptions = {
|
|
|
134
134
|
* };
|
|
135
135
|
* ```
|
|
136
136
|
*/
|
|
137
|
-
type HasOneOptions = {
|
|
137
|
+
export type HasOneOptions = {
|
|
138
138
|
/**
|
|
139
139
|
* The foreign key field on the related model.
|
|
140
140
|
*
|
|
@@ -165,7 +165,7 @@ type HasOneOptions = {
|
|
|
165
165
|
* };
|
|
166
166
|
* ```
|
|
167
167
|
*/
|
|
168
|
-
type BelongsToOptions = {
|
|
168
|
+
export type BelongsToOptions = {
|
|
169
169
|
/**
|
|
170
170
|
* The foreign key field on this model that references the related model.
|
|
171
171
|
*
|
|
@@ -198,7 +198,7 @@ type BelongsToOptions = {
|
|
|
198
198
|
* };
|
|
199
199
|
* ```
|
|
200
200
|
*/
|
|
201
|
-
type BelongsToManyOptions = {
|
|
201
|
+
export type BelongsToManyOptions = {
|
|
202
202
|
/**
|
|
203
203
|
* The pivot table name that connects the two models.
|
|
204
204
|
*
|
|
@@ -245,7 +245,7 @@ type BelongsToManyOptions = {
|
|
|
245
245
|
* });
|
|
246
246
|
* ```
|
|
247
247
|
*/
|
|
248
|
-
type RelationConstraintCallback = (query: QueryBuilderContract) => void;
|
|
248
|
+
export type RelationConstraintCallback = (query: QueryBuilderContract) => void;
|
|
249
249
|
/**
|
|
250
250
|
* Constraints to apply when loading relations.
|
|
251
251
|
*
|
|
@@ -262,14 +262,14 @@ type RelationConstraintCallback = (query: QueryBuilderContract) => void;
|
|
|
262
262
|
* });
|
|
263
263
|
* ```
|
|
264
264
|
*/
|
|
265
|
-
type RelationConstraints = Record<string, boolean | RelationConstraintCallback>;
|
|
265
|
+
export type RelationConstraints = Record<string, boolean | RelationConstraintCallback>;
|
|
266
266
|
/**
|
|
267
267
|
* Type for the result of loading a relation.
|
|
268
268
|
*
|
|
269
269
|
* - For hasOne/belongsTo: A single model instance or null
|
|
270
270
|
* - For hasMany/belongsToMany: An array of model instances
|
|
271
271
|
*/
|
|
272
|
-
type LoadedRelationResult = Model | Model[] | null;
|
|
272
|
+
export type LoadedRelationResult = Model | Model[] | null;
|
|
273
273
|
/**
|
|
274
274
|
* Map that stores loaded relation data on a model instance.
|
|
275
275
|
*
|
|
@@ -280,7 +280,7 @@ type LoadedRelationResult = Model | Model[] | null;
|
|
|
280
280
|
* user.loadedRelations.get("profile"); // Profile | null
|
|
281
281
|
* ```
|
|
282
282
|
*/
|
|
283
|
-
type LoadedRelationsMap = Map<string, LoadedRelationResult>;
|
|
283
|
+
export type LoadedRelationsMap = Map<string, LoadedRelationResult>;
|
|
284
284
|
/**
|
|
285
285
|
* A map of relation names to their definitions.
|
|
286
286
|
*
|
|
@@ -297,7 +297,7 @@ type LoadedRelationsMap = Map<string, LoadedRelationResult>;
|
|
|
297
297
|
* }
|
|
298
298
|
* ```
|
|
299
299
|
*/
|
|
300
|
-
type RelationDefinitions = Record<string, RelationDefinition>;
|
|
300
|
+
export type RelationDefinitions = Record<string, RelationDefinition>;
|
|
301
301
|
/**
|
|
302
302
|
* Additional data to store in the pivot table for many-to-many relationships.
|
|
303
303
|
*
|
|
@@ -306,12 +306,11 @@ type RelationDefinitions = Record<string, RelationDefinition>;
|
|
|
306
306
|
* await post.attach("tags", [1, 2, 3], { addedBy: userId, addedAt: new Date() });
|
|
307
307
|
* ```
|
|
308
308
|
*/
|
|
309
|
-
type PivotData = Record<string, unknown>;
|
|
309
|
+
export type PivotData = Record<string, unknown>;
|
|
310
310
|
/**
|
|
311
311
|
* IDs that can be used for pivot operations.
|
|
312
312
|
* Supports both numeric and string IDs.
|
|
313
313
|
*/
|
|
314
|
-
type PivotIds = (number | string)[];
|
|
314
|
+
export type PivotIds = (number | string)[];
|
|
315
315
|
//#endregion
|
|
316
|
-
export { BelongsToManyOptions, BelongsToOptions, HasManyOptions, HasOneOptions, LoadedRelationResult, LoadedRelationsMap, PivotData, PivotIds, RelationConstraintCallback, RelationConstraints, RelationDefinition, RelationDefinitions, RelationType };
|
|
317
316
|
//# sourceMappingURL=types.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/types.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;YAiCY;;;;;;;;;;;;;;;;;YAsBA;;;;WAID,MAAM;;;;;;;;;;;;;;;;WAiBN,gBAAgB,WAAW,SAAS;;;;;;;WAQpC;;;;;;WAOA;;;;WAKA;;;;;WAMA;;;;;WAMA;;;;;;WAOA;;;;;;;;;;;;;YAkBC;;;;;;WAMD;;;;;;WAOA;;;;;;WAOA;;;;;;;;;;;;;YAcC;;;;;;WAMD;;;;;;WAOA;;;;;;WAOA;;;;;;;;;;;;;YAcC;;;;;;WAMD;;;;;;WAOA;;;;WAKA;;;;;;;;;;;;;;;;;YAkBC;;;;;;;WAOD;;;;;;WAOA;;;;;;WAOA;;;;;;WAOA;;;;;;WAOA;;;;WAKA;;;;;;;;;;;;YAiBC,8BAA8B,OAAO;;;;;;;;;;;;;;;;;YAkBrC,sBAAsB,yBAAyB;;;;;;;YAY/C,uBAAuB,QAAQ;;;;;;;;;;;YAY/B,qBAAqB,YAAY;;;;;;;;;;;;;;;;;YAsBjC,sBAAsB,eAAe;;;;;;;;;YAcrC,YAAY;;;;;YAMZ"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Model } from "../model/model.mjs";
|
|
2
2
|
import { RemoverContract, RemoverOptions, RemoverResult } from "../contracts/database-remover.contract.mjs";
|
|
3
|
-
|
|
4
3
|
//#region ../../packages/cascade/src/remover/database-remover.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Database remover service that orchestrates model deletion.
|
|
@@ -22,7 +21,7 @@ import { RemoverContract, RemoverOptions, RemoverResult } from "../contracts/dat
|
|
|
22
21
|
* console.log(result.strategy); // "trash" | "permanent" | "soft"
|
|
23
22
|
* ```
|
|
24
23
|
*/
|
|
25
|
-
declare class DatabaseRemover implements RemoverContract {
|
|
24
|
+
export declare class DatabaseRemover implements RemoverContract {
|
|
26
25
|
/** The model instance being deleted */
|
|
27
26
|
private readonly model;
|
|
28
27
|
/** Model constructor reference */
|
|
@@ -100,5 +99,4 @@ declare class DatabaseRemover implements RemoverContract {
|
|
|
100
99
|
private triggerSync;
|
|
101
100
|
}
|
|
102
101
|
//#endregion
|
|
103
|
-
export { DatabaseRemover };
|
|
104
102
|
//# sourceMappingURL=database-remover.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;qBAiCa,2BAA2B;;mBAErB;;mBAGA;;mBAGA;;mBAGA;;mBAGA;;mBAGA;;;;;;;;;;;;;EAcjB,YAAmB,OAAO;;;;;;;;EAgBb,QAAQ,UAAS,iBAAsB,QAAQ;;;;;;;;;;;;;;;;;;;;;UAoKpD;;;;;;;;;;;;UAsBA;;;;;;;;;UAoBA"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Model } from "../model/model.mjs";
|
|
2
2
|
import { RestorerContract, RestorerOptions, RestorerResult } from "../contracts/database-restorer.contract.mjs";
|
|
3
|
-
|
|
4
3
|
//#region ../../packages/cascade/src/restorer/database-restorer.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Database restorer service that orchestrates model restoration.
|
|
@@ -21,7 +20,7 @@ import { RestorerContract, RestorerOptions, RestorerResult } from "../contracts/
|
|
|
21
20
|
* console.log(result.strategy); // "trash" | "soft"
|
|
22
21
|
* ```
|
|
23
22
|
*/
|
|
24
|
-
declare class DatabaseRestorer implements RestorerContract {
|
|
23
|
+
export declare class DatabaseRestorer implements RestorerContract {
|
|
25
24
|
/** Model constructor reference */
|
|
26
25
|
private readonly ctor;
|
|
27
26
|
/** Data source containing driver */
|
|
@@ -140,5 +139,4 @@ declare class DatabaseRestorer implements RestorerContract {
|
|
|
140
139
|
private triggerSync;
|
|
141
140
|
}
|
|
142
141
|
//#endregion
|
|
143
|
-
export { DatabaseRestorer };
|
|
144
142
|
//# sourceMappingURL=database-restorer.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-restorer.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/restorer/database-restorer.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"database-restorer.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/restorer/database-restorer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;qBAgCa,4BAA4B;;mBAEtB;;mBAGA;;mBAGA;;mBAGA;;mBAGA;;;;;;;;;;;;EAajB,YAAmB,mBAAmB;;;;;;;;;;EAiBzB,QACX,qBACA,UAAS,kBACR,QAAQ;;;;;;;EA2FE,WAAW,UAAS,kBAAuB,QAAQ;;;;;;;;;;UAsKxD;;;;;;;;;UAiBM;;;;;;;;UA2CA;;;;;;;;;;UA6CA;;;;;;;;UA4BA;;;;;;;;;;;;;UAsBA;;;;;;;;;;;;UAkCN;;;;;;;UAkBA"}
|
|
@@ -307,7 +307,8 @@ var DatabaseRestorer = class {
|
|
|
307
307
|
}
|
|
308
308
|
delete newData._id;
|
|
309
309
|
delete newData.id;
|
|
310
|
-
|
|
310
|
+
const model = new this.ctor(newData);
|
|
311
|
+
return await new DatabaseWriter(model).prepareForInsert({
|
|
311
312
|
skipEvents: true,
|
|
312
313
|
skipValidation: true
|
|
313
314
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-restorer.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/restorer/database-restorer.ts"],"sourcesContent":["import type { DriverContract, UpdateOperations } from \"../contracts/database-driver.contract\";\nimport type {\n RestorerContract,\n RestorerOptions,\n RestorerResult,\n} from \"../contracts/database-restorer.contract\";\nimport type { DataSource } from \"../data-source/data-source\";\nimport type { Model } from \"../model/model\";\nimport type { DeleteStrategy } from \"../types\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { DatabaseWriter } from \"../writer/database-writer\";\n\n/**\n * Database restorer service that orchestrates model restoration.\n *\n * Handles the complete restoration pipeline:\n * 1. Strategy detection (trash vs soft delete)\n * 2. Record retrieval from trash table or soft-deleted records\n * 3. ID conflict resolution\n * 4. Event emission (restoring, restored)\n * 5. Driver execution (insert back to original table, remove from trash/clear deletedAt)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * const result = await restorer.restore(123);\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"soft\"\n * ```\n */\nexport class DatabaseRestorer implements RestorerContract {\n /** Model constructor reference */\n private readonly ctor: typeof Model;\n\n /** Data source containing driver */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /**\n * Create a new restorer instance for a model class.\n *\n * @param modelClass - The model class (static context)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * await restorer.restore(123);\n * ```\n */\n public constructor(modelClass: typeof Model) {\n this.ctor = modelClass;\n this.dataSource = modelClass.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = modelClass.table;\n this.primaryKey = modelClass.primaryKey;\n }\n\n /**\n * Restore a single deleted record by its ID.\n *\n * @param id - The primary key value of the record to restore\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and restored record\n * @throws {Error} If record not found in trash or soft-deleted records\n * @throws {Error} If ID conflict and onIdConflict is \"fail\"\n */\n public async restore(\n id: string | number,\n options: RestorerOptions = {},\n ): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch record based on strategy\n const recordData = await this.fetchRecordByStrategy(id, strategy);\n\n if (!recordData) {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: record not found in ${strategy === \"trash\" ? \"trash table\" : \"soft-deleted records\"}.`,\n );\n }\n\n // 4. Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // 5. Check for ID conflict and handle. A soft-deleted row never left the\n // table, so it would \"conflict\" with itself: it keeps its id and is\n // updated in place.\n const finalData =\n strategy === \"trash\"\n ? await this.handleIdConflict(restoredData, id, onIdConflict)\n : restoredData;\n\n // 6. Create temporary model instance for event emission\n // Note: Model is abstract, but at runtime this.ctor is a concrete subclass\n const model = new (this.ctor as any)(finalData) as Model;\n\n // 7. Emit restoring event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // 8. Restore based on strategy\n if (strategy === \"trash\") {\n // Insert record back into original table (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n\n // Remove from trash table\n await this.driver.delete(this.resolveTrashTable(), {\n [this.primaryKey]: id,\n originalTable: this.table,\n });\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt (don't insert - would create duplicate!)\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n // 10. Emit restored event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n\n return {\n success: true,\n restoredCount: 1,\n strategy,\n restoredRecord: model,\n };\n }\n\n /**\n * Restore all deleted records for the model's table.\n *\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and aggregate counts\n */\n public async restoreAll(options: RestorerOptions = {}): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore all ${this.ctor.name} records: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch all records based on strategy\n const recordsToRestore = await this.fetchAllRecordsByStrategy(strategy);\n\n if (recordsToRestore.length === 0) {\n return {\n success: true,\n restoredCount: 0,\n strategy,\n };\n }\n\n // 4. Restore each record\n let restoredCount = 0;\n const conflicts: Array<{ id: string | number; reason: string }> = [];\n\n const restoredRecords: Model[] = [];\n\n for (const recordData of recordsToRestore) {\n const id = recordData[this.primaryKey] as string | number;\n\n try {\n // Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // Check for ID conflict\n // Soft rows never left the table; only trash restores can conflict.\n const idExists = strategy === \"trash\" && (await this.checkIdExists(id));\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n const finalData = await this.assignNewId(restoredData);\n conflicts.push({\n id,\n reason: `ID ${id} already exists, assigned new ID ${finalData[this.primaryKey]}`,\n });\n\n // Create temporary model for events\n const model = new (this.ctor as any)(finalData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with new ID (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n } else {\n // No conflict, restore with original ID\n const model = new (this.ctor as any)(restoredData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with original ID (was moved to trash)\n await this.driver.insert(this.table, restoredData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n }\n\n // Remove from trash (only for trash strategy)\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n const trashFilter = { [this.primaryKey]: id, originalTable: this.table };\n await this.driver.delete(trashTable, trashFilter);\n }\n\n restoredCount++;\n } catch (error) {\n if (onIdConflict === \"fail\") {\n throw error;\n }\n // Continue with next record if assignNew mode\n conflicts.push({\n id,\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return {\n success: true,\n restoredCount,\n restoredRecords,\n strategy,\n conflicts: conflicts.length > 0 ? conflicts : undefined,\n };\n }\n\n /**\n * Resolve the delete strategy.\n *\n * Priority: options → model static → data source default → \"permanent\"\n *\n * @param strategyOption - Optional strategy override from options\n * @returns The resolved delete strategy\n * @private\n */\n private resolveStrategy(strategyOption?: \"trash\" | \"soft\"): DeleteStrategy {\n return (\n strategyOption ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\"\n );\n }\n\n /**\n * Fetch a record by ID based on the delete strategy.\n *\n * @param id - The primary key value\n * @param strategy - The delete strategy to use\n * @returns The record data, or null if not found\n * @private\n */\n private async fetchRecordByStrategy(id: string | number, strategy: \"trash\" | \"soft\") {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = await this.driver\n .queryBuilder(trashTable)\n .where(this.primaryKey, id)\n .where(\"originalTable\", this.table)\n .first<Record<string, unknown>>();\n\n return trashQuery;\n } catch {\n return null;\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return null;\n }\n\n try {\n const softDeletedQuery = await this.driver\n .queryBuilder(this.table)\n .where(this.primaryKey, id)\n .whereNotNull(deletedAtColumn)\n .first<Record<string, unknown>>();\n\n return softDeletedQuery;\n } catch {\n return null;\n }\n }\n\n return null;\n }\n\n /**\n * Fetch all records based on the delete strategy.\n *\n * @param strategy - The delete strategy to use\n * @returns Array of record data\n * @private\n */\n private async fetchAllRecordsByStrategy(\n strategy: \"trash\" | \"soft\",\n ): Promise<Record<string, unknown>[]> {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = this.driver\n .queryBuilder(trashTable)\n .where(\"originalTable\", this.table)\n .get();\n\n return (await trashQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return [];\n }\n\n try {\n const softDeletedQuery = this.driver\n .queryBuilder(this.table)\n .whereNotNull(deletedAtColumn)\n .get();\n\n return (await softDeletedQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n }\n\n return [];\n }\n\n /**\n * Handle ID conflict by checking if ID exists and assigning new one if needed.\n *\n * @param recordData - The record data to restore\n * @param originalId - The original ID value\n * @param onIdConflict - Conflict resolution strategy\n * @returns Record data with potentially new ID\n * @private\n */\n private async handleIdConflict(\n recordData: Record<string, unknown>,\n originalId: string | number,\n onIdConflict: \"fail\" | \"assignNew\",\n ): Promise<Record<string, unknown>> {\n const idExists = await this.checkIdExists(originalId);\n\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${originalId}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n return await this.assignNewId(recordData);\n }\n\n return recordData;\n }\n\n /**\n * Check if an ID already exists in the target table.\n *\n * @param id - The ID to check\n * @returns True if ID exists, false otherwise\n * @private\n */\n private async checkIdExists(id: string | number): Promise<boolean> {\n try {\n const query = this.driver.queryBuilder(this.table).where(this.primaryKey, id).exists();\n\n return await query;\n } catch {\n return false;\n }\n }\n\n /**\n * Assign a new ID to the record data.\n *\n * For MongoDB: drops the old `_id` and `id`, then runs the record through the\n * writer's insert preparation so the new `id` comes from the same generator\n * a normal save uses (validation and events are skipped; this is a restore)\n * For SQL: Removes the primary key to let database auto-increment\n *\n * @param recordData - The record data\n * @returns Record data with new ID assigned\n * @private\n */\n private async assignNewId(recordData: Record<string, unknown>): Promise<Record<string, unknown>> {\n const newData = { ...recordData };\n\n if (this.driver.name !== \"mongodb\") {\n // SQL: Remove primary key to let database auto-increment\n delete newData[this.primaryKey];\n\n return newData;\n }\n\n // MongoDB: let the driver mint a new _id and the writer generate a new id.\n // The generator skips records that already carry an `id`, so drop both.\n delete newData._id;\n delete newData.id;\n\n const model = new (this.ctor as any)(newData) as Model;\n\n return await new DatabaseWriter(model).prepareForInsert({\n skipEvents: true,\n skipValidation: true,\n });\n }\n\n /**\n * Resolve the trash table/collection name.\n *\n * Priority:\n * 1. Model.trashTable (if set)\n * 2. Data source defaultTrashTable (e.g., \"RecycleBin\" for MongoDB)\n * 3. Default pattern: `{table}Trash`\n *\n * @returns The trash table/collection name\n * @private\n */\n private resolveTrashTable(): string {\n if (this.ctor.trashTable) {\n return this.ctor.trashTable;\n }\n\n if (this.dataSource.defaultTrashTable) {\n return this.dataSource.defaultTrashTable;\n }\n\n return `${this.table}Trash`;\n }\n\n /**\n * Queue sync for a restored record so embedded copies come back up to date.\n * The restore has already succeeded, so queued failures are retried separately.\n *\n * @private\n */\n private triggerSync(model: Model): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"restore\",\n execute: () =>\n triggerModelEvent(\n this.ctor as any,\n \"updated\",\n model,\n Object.keys(model.data as Record<string, unknown>),\n ),\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database-restorer.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/restorer/database-restorer.ts"],"sourcesContent":["import type { DriverContract, UpdateOperations } from \"../contracts/database-driver.contract\";\nimport type {\n RestorerContract,\n RestorerOptions,\n RestorerResult,\n} from \"../contracts/database-restorer.contract\";\nimport type { DataSource } from \"../data-source/data-source\";\nimport type { Model } from \"../model/model\";\nimport type { DeleteStrategy } from \"../types\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { DatabaseWriter } from \"../writer/database-writer\";\n\n/**\n * Database restorer service that orchestrates model restoration.\n *\n * Handles the complete restoration pipeline:\n * 1. Strategy detection (trash vs soft delete)\n * 2. Record retrieval from trash table or soft-deleted records\n * 3. ID conflict resolution\n * 4. Event emission (restoring, restored)\n * 5. Driver execution (insert back to original table, remove from trash/clear deletedAt)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * const result = await restorer.restore(123);\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"soft\"\n * ```\n */\nexport class DatabaseRestorer implements RestorerContract {\n /** Model constructor reference */\n private readonly ctor: typeof Model;\n\n /** Data source containing driver */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /**\n * Create a new restorer instance for a model class.\n *\n * @param modelClass - The model class (static context)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * await restorer.restore(123);\n * ```\n */\n public constructor(modelClass: typeof Model) {\n this.ctor = modelClass;\n this.dataSource = modelClass.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = modelClass.table;\n this.primaryKey = modelClass.primaryKey;\n }\n\n /**\n * Restore a single deleted record by its ID.\n *\n * @param id - The primary key value of the record to restore\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and restored record\n * @throws {Error} If record not found in trash or soft-deleted records\n * @throws {Error} If ID conflict and onIdConflict is \"fail\"\n */\n public async restore(\n id: string | number,\n options: RestorerOptions = {},\n ): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch record based on strategy\n const recordData = await this.fetchRecordByStrategy(id, strategy);\n\n if (!recordData) {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: record not found in ${strategy === \"trash\" ? \"trash table\" : \"soft-deleted records\"}.`,\n );\n }\n\n // 4. Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // 5. Check for ID conflict and handle. A soft-deleted row never left the\n // table, so it would \"conflict\" with itself: it keeps its id and is\n // updated in place.\n const finalData =\n strategy === \"trash\"\n ? await this.handleIdConflict(restoredData, id, onIdConflict)\n : restoredData;\n\n // 6. Create temporary model instance for event emission\n // Note: Model is abstract, but at runtime this.ctor is a concrete subclass\n const model = new (this.ctor as any)(finalData) as Model;\n\n // 7. Emit restoring event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // 8. Restore based on strategy\n if (strategy === \"trash\") {\n // Insert record back into original table (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n\n // Remove from trash table\n await this.driver.delete(this.resolveTrashTable(), {\n [this.primaryKey]: id,\n originalTable: this.table,\n });\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt (don't insert - would create duplicate!)\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n // 10. Emit restored event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n\n return {\n success: true,\n restoredCount: 1,\n strategy,\n restoredRecord: model,\n };\n }\n\n /**\n * Restore all deleted records for the model's table.\n *\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and aggregate counts\n */\n public async restoreAll(options: RestorerOptions = {}): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore all ${this.ctor.name} records: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch all records based on strategy\n const recordsToRestore = await this.fetchAllRecordsByStrategy(strategy);\n\n if (recordsToRestore.length === 0) {\n return {\n success: true,\n restoredCount: 0,\n strategy,\n };\n }\n\n // 4. Restore each record\n let restoredCount = 0;\n const conflicts: Array<{ id: string | number; reason: string }> = [];\n\n const restoredRecords: Model[] = [];\n\n for (const recordData of recordsToRestore) {\n const id = recordData[this.primaryKey] as string | number;\n\n try {\n // Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // Check for ID conflict\n // Soft rows never left the table; only trash restores can conflict.\n const idExists = strategy === \"trash\" && (await this.checkIdExists(id));\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n const finalData = await this.assignNewId(restoredData);\n conflicts.push({\n id,\n reason: `ID ${id} already exists, assigned new ID ${finalData[this.primaryKey]}`,\n });\n\n // Create temporary model for events\n const model = new (this.ctor as any)(finalData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with new ID (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n } else {\n // No conflict, restore with original ID\n const model = new (this.ctor as any)(restoredData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with original ID (was moved to trash)\n await this.driver.insert(this.table, restoredData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n }\n\n // Remove from trash (only for trash strategy)\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n const trashFilter = { [this.primaryKey]: id, originalTable: this.table };\n await this.driver.delete(trashTable, trashFilter);\n }\n\n restoredCount++;\n } catch (error) {\n if (onIdConflict === \"fail\") {\n throw error;\n }\n // Continue with next record if assignNew mode\n conflicts.push({\n id,\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return {\n success: true,\n restoredCount,\n restoredRecords,\n strategy,\n conflicts: conflicts.length > 0 ? conflicts : undefined,\n };\n }\n\n /**\n * Resolve the delete strategy.\n *\n * Priority: options → model static → data source default → \"permanent\"\n *\n * @param strategyOption - Optional strategy override from options\n * @returns The resolved delete strategy\n * @private\n */\n private resolveStrategy(strategyOption?: \"trash\" | \"soft\"): DeleteStrategy {\n return (\n strategyOption ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\"\n );\n }\n\n /**\n * Fetch a record by ID based on the delete strategy.\n *\n * @param id - The primary key value\n * @param strategy - The delete strategy to use\n * @returns The record data, or null if not found\n * @private\n */\n private async fetchRecordByStrategy(id: string | number, strategy: \"trash\" | \"soft\") {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = await this.driver\n .queryBuilder(trashTable)\n .where(this.primaryKey, id)\n .where(\"originalTable\", this.table)\n .first<Record<string, unknown>>();\n\n return trashQuery;\n } catch {\n return null;\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return null;\n }\n\n try {\n const softDeletedQuery = await this.driver\n .queryBuilder(this.table)\n .where(this.primaryKey, id)\n .whereNotNull(deletedAtColumn)\n .first<Record<string, unknown>>();\n\n return softDeletedQuery;\n } catch {\n return null;\n }\n }\n\n return null;\n }\n\n /**\n * Fetch all records based on the delete strategy.\n *\n * @param strategy - The delete strategy to use\n * @returns Array of record data\n * @private\n */\n private async fetchAllRecordsByStrategy(\n strategy: \"trash\" | \"soft\",\n ): Promise<Record<string, unknown>[]> {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = this.driver\n .queryBuilder(trashTable)\n .where(\"originalTable\", this.table)\n .get();\n\n return (await trashQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return [];\n }\n\n try {\n const softDeletedQuery = this.driver\n .queryBuilder(this.table)\n .whereNotNull(deletedAtColumn)\n .get();\n\n return (await softDeletedQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n }\n\n return [];\n }\n\n /**\n * Handle ID conflict by checking if ID exists and assigning new one if needed.\n *\n * @param recordData - The record data to restore\n * @param originalId - The original ID value\n * @param onIdConflict - Conflict resolution strategy\n * @returns Record data with potentially new ID\n * @private\n */\n private async handleIdConflict(\n recordData: Record<string, unknown>,\n originalId: string | number,\n onIdConflict: \"fail\" | \"assignNew\",\n ): Promise<Record<string, unknown>> {\n const idExists = await this.checkIdExists(originalId);\n\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${originalId}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n return await this.assignNewId(recordData);\n }\n\n return recordData;\n }\n\n /**\n * Check if an ID already exists in the target table.\n *\n * @param id - The ID to check\n * @returns True if ID exists, false otherwise\n * @private\n */\n private async checkIdExists(id: string | number): Promise<boolean> {\n try {\n const query = this.driver.queryBuilder(this.table).where(this.primaryKey, id).exists();\n\n return await query;\n } catch {\n return false;\n }\n }\n\n /**\n * Assign a new ID to the record data.\n *\n * For MongoDB: drops the old `_id` and `id`, then runs the record through the\n * writer's insert preparation so the new `id` comes from the same generator\n * a normal save uses (validation and events are skipped; this is a restore)\n * For SQL: Removes the primary key to let database auto-increment\n *\n * @param recordData - The record data\n * @returns Record data with new ID assigned\n * @private\n */\n private async assignNewId(recordData: Record<string, unknown>): Promise<Record<string, unknown>> {\n const newData = { ...recordData };\n\n if (this.driver.name !== \"mongodb\") {\n // SQL: Remove primary key to let database auto-increment\n delete newData[this.primaryKey];\n\n return newData;\n }\n\n // MongoDB: let the driver mint a new _id and the writer generate a new id.\n // The generator skips records that already carry an `id`, so drop both.\n delete newData._id;\n delete newData.id;\n\n const model = new (this.ctor as any)(newData) as Model;\n\n return await new DatabaseWriter(model).prepareForInsert({\n skipEvents: true,\n skipValidation: true,\n });\n }\n\n /**\n * Resolve the trash table/collection name.\n *\n * Priority:\n * 1. Model.trashTable (if set)\n * 2. Data source defaultTrashTable (e.g., \"RecycleBin\" for MongoDB)\n * 3. Default pattern: `{table}Trash`\n *\n * @returns The trash table/collection name\n * @private\n */\n private resolveTrashTable(): string {\n if (this.ctor.trashTable) {\n return this.ctor.trashTable;\n }\n\n if (this.dataSource.defaultTrashTable) {\n return this.dataSource.defaultTrashTable;\n }\n\n return `${this.table}Trash`;\n }\n\n /**\n * Queue sync for a restored record so embedded copies come back up to date.\n * The restore has already succeeded, so queued failures are retried separately.\n *\n * @private\n */\n private triggerSync(model: Model): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"restore\",\n execute: () =>\n triggerModelEvent(\n this.ctor as any,\n \"updated\",\n model,\n Object.keys(model.data as Record<string, unknown>),\n ),\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { DatabaseDirtyTracker } from "./database-dirty-tracker.mjs";
|
|
2
|
-
|
|
3
2
|
//#region ../../packages/cascade/src/sql-database-dirty-tracker.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* A dirty tracker designed for SQL databases where nested objects shouldn't be flattened.
|
|
6
5
|
* Since SQL drivers (like Postgres) replace the entire JSON column when updated,
|
|
7
6
|
* we keep the object intact instead of using dot-notation keys.
|
|
8
7
|
*/
|
|
9
|
-
declare class SqlDatabaseDirtyTracker extends DatabaseDirtyTracker {
|
|
8
|
+
export declare class SqlDatabaseDirtyTracker extends DatabaseDirtyTracker {
|
|
10
9
|
/**
|
|
11
10
|
* Overrides the default flattening behavior to keep the raw data structure.
|
|
12
11
|
*/
|
|
13
12
|
protected flattenData(data: Record<string, unknown>): Record<string, unknown>;
|
|
14
13
|
}
|
|
15
14
|
//#endregion
|
|
16
|
-
export { SqlDatabaseDirtyTracker };
|
|
17
15
|
//# sourceMappingURL=sql-database-dirty-tracker.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-database-dirty-tracker.d.mts","names":[],"sources":["../../../../../../../packages/cascade/src/sql-database-dirty-tracker.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"sql-database-dirty-tracker.d.mts","names":[],"sources":["../../../../../../../packages/cascade/src/sql-database-dirty-tracker.ts"],"mappings":";;;;;;;qBAOa,gCAAgC;;;;YAIxB,YAAY,MAAM,0BAA0B"}
|
|
@@ -102,5 +102,5 @@ async function triggerModelEvent(modelClass, eventType, ...args) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
//#endregion
|
|
105
|
-
export { getModelDeletedEvent, getModelUpdatedEvent, triggerModelEvent };
|
|
105
|
+
export { MODEL_EVENT_PREFIX, ModelSyncEventType, getLegacyModelEvent, getModelDeletedEvent, getModelEvent, getModelUpdatedEvent, triggerModelEvent };
|
|
106
106
|
//# sourceMappingURL=model-events.mjs.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { ChildModel } from "../model/model.types.mjs";
|
|
2
1
|
import { ModelSyncConfig, ModelSyncOperationContract } from "./types.mjs";
|
|
2
|
+
import { ChildModel } from "../model/model.types.mjs";
|
|
3
3
|
import { Model } from "../model/model.mjs";
|
|
4
|
-
|
|
5
4
|
//#region ../../packages/cascade/src/sync/model-sync-operation.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* Manages a single model sync operation.
|
|
@@ -19,7 +18,7 @@ import { Model } from "../model/model.mjs";
|
|
|
19
18
|
* .unsetOnDelete();
|
|
20
19
|
* ```
|
|
21
20
|
*/
|
|
22
|
-
declare class ModelSyncOperation implements ModelSyncOperationContract {
|
|
21
|
+
export declare class ModelSyncOperation implements ModelSyncOperationContract {
|
|
23
22
|
/**
|
|
24
23
|
* Configuration for this sync operation.
|
|
25
24
|
*/
|
|
@@ -155,5 +154,4 @@ declare class ModelSyncOperation implements ModelSyncOperationContract {
|
|
|
155
154
|
getConfig(): Readonly<ModelSyncConfig>;
|
|
156
155
|
}
|
|
157
156
|
//#endregion
|
|
158
|
-
export { ModelSyncOperation };
|
|
159
157
|
//# sourceMappingURL=model-sync-operation.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-sync-operation.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/model-sync-operation.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"model-sync-operation.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/model-sync-operation.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;qBAkCa,8BAA8B;;;;mBAIxB;;;;mBAKA;;;;UAKT;;;;;;;;;EAUR,YACE,kBAAkB,WAAW,QAC7B,kBAAkB,WAAW,QAC7B,qBACA;;;;;;;;;;;;EAiCK,MAAM;;;;;;;;;;;;;EAiBN,WAAW;;;;;;;;;;;;EAgBX,SAAS;;;;;;;;;;;;;EAiBT,YAAY;;;;;;;;;;;EAeZ;;;;;;;;;;;EAeA;;;;;UAaC;;;;;EAwBD;;;;EAYA;;;;;UAYC;;;;;UAuCA;;;;UAwCA;;;;UAWA;;;;UAiBM;;;;EAWP,aAAa,SAAS"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { ChildModel } from "../model/model.types.mjs";
|
|
2
1
|
import { ModelSyncContract, ModelSyncOperationContract } from "./types.mjs";
|
|
2
|
+
import { ChildModel } from "../model/model.types.mjs";
|
|
3
3
|
import { Model } from "../model/model.mjs";
|
|
4
|
-
|
|
5
4
|
//#region ../../packages/cascade/src/sync/model-sync.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* ModelSync facade implementation.
|
|
@@ -120,7 +119,7 @@ declare class ModelSyncFacade implements ModelSyncContract {
|
|
|
120
119
|
* });
|
|
121
120
|
* ```
|
|
122
121
|
*/
|
|
123
|
-
declare const modelSync: ModelSyncFacade;
|
|
122
|
+
export declare const modelSync: ModelSyncFacade;
|
|
124
123
|
//#endregion
|
|
125
|
-
export {
|
|
124
|
+
export {};
|
|
126
125
|
//# sourceMappingURL=model-sync.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-sync.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/model-sync.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"model-sync.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/model-sync.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;cA6BM,2BAA2B;;;;mBAId;;;;;mBAMA;;;;;;;;;;;;;;;;;;;;;;;EA4BV,KACL,QAAQ,WAAW,QACnB,QAAQ,WAAW,QACnB,gBACC;;;;;;;;;;;;;;;;;;EAuBI,SACL,QAAQ,WAAW,QACnB,QAAQ,WAAW,QACnB,gBACC;;;;;;;;;;;;;;;;;;;;;EA0BI,SAAS;;;;;EA+BT;;;;;MAWI;;;;UAWH;;;;;;;;;;;;;;;;qBA0BG,WAAS"}
|