@warlock.js/cascade 5.19.1 → 5.21.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 +36 -1
- package/cjs/index.cjs +2356 -1039
- package/cjs/index.cjs.map +1 -1
- package/esm/context/database-transaction-context.d.mts +10 -0
- package/esm/context/database-transaction-context.d.mts.map +1 -1
- package/esm/context/database-transaction-context.mjs +17 -0
- package/esm/context/database-transaction-context.mjs.map +1 -1
- package/esm/contracts/query-builder.contract.d.mts +30 -3
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.d.mts +33 -1
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.mjs +75 -5
- package/esm/data-source/data-source-registry.mjs.map +1 -1
- package/esm/database-dirty-tracker.d.mts +5 -0
- package/esm/database-dirty-tracker.d.mts.map +1 -1
- package/esm/database-dirty-tracker.mjs +16 -0
- package/esm/database-dirty-tracker.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +12 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +58 -19
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs +15 -18
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +13 -3
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +72 -19
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +6 -0
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +128 -30
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/index.d.mts +2 -3
- package/esm/drivers/postgres/index.mjs +1 -2
- package/esm/drivers/postgres/postgres-blueprint.mjs +4 -4
- package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts +5 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.mjs +38 -8
- package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +52 -29
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +196 -58
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.mts +2 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.mjs +8 -6
- package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +18 -7
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +146 -31
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +23 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +160 -43
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-sql-serializer.mjs +3 -2
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-type-converters.mjs +57 -0
- package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -0
- package/esm/events/model-events.d.mts +1 -1
- package/esm/events/model-events.d.mts.map +1 -1
- package/esm/events/model-events.mjs.map +1 -1
- package/esm/index.d.mts +6 -6
- package/esm/index.mjs +5 -5
- package/esm/migration/column-builder.d.mts.map +1 -1
- package/esm/migration/column-builder.mjs +2 -2
- package/esm/migration/column-builder.mjs.map +1 -1
- package/esm/migration/index.d.mts +2 -2
- package/esm/migration/index.mjs +1 -1
- package/esm/migration/migration-order.mjs +5 -2
- package/esm/migration/migration-order.mjs.map +1 -1
- package/esm/migration/migration-runner.d.mts +50 -5
- package/esm/migration/migration-runner.d.mts.map +1 -1
- package/esm/migration/migration-runner.mjs +370 -147
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/esm/migration/migration.d.mts +36 -3
- package/esm/migration/migration.d.mts.map +1 -1
- package/esm/migration/migration.mjs +62 -5
- package/esm/migration/migration.mjs.map +1 -1
- package/esm/model/methods/accessor-methods.mjs +1 -3
- package/esm/model/methods/accessor-methods.mjs.map +1 -1
- package/esm/model/methods/delete-methods.mjs +6 -2
- package/esm/model/methods/delete-methods.mjs.map +1 -1
- package/esm/model/methods/hydration-methods.mjs +16 -2
- package/esm/model/methods/hydration-methods.mjs.map +1 -1
- package/esm/model/methods/instance-event-methods.mjs +2 -1
- package/esm/model/methods/instance-event-methods.mjs.map +1 -1
- package/esm/model/methods/meta-methods.mjs +53 -16
- package/esm/model/methods/meta-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs +86 -15
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/methods/static-event-methods.mjs +16 -1
- package/esm/model/methods/static-event-methods.mjs.map +1 -1
- package/esm/model/methods/write-methods.d.mts.map +1 -1
- package/esm/model/methods/write-methods.mjs +50 -50
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +16 -1
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs +39 -5
- package/esm/model/model.mjs.map +1 -1
- package/esm/model/register-model.d.mts +6 -1
- package/esm/model/register-model.d.mts.map +1 -1
- package/esm/model/register-model.mjs +16 -1
- package/esm/model/register-model.mjs.map +1 -1
- package/esm/query-builder/query-builder.d.mts +21 -2
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs +97 -20
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/relations/key-conventions.mjs +13 -3
- package/esm/relations/key-conventions.mjs.map +1 -1
- package/esm/relations/pivot-operations.mjs +1 -1
- package/esm/relations/relation-loader.d.mts +15 -5
- package/esm/relations/relation-loader.d.mts.map +1 -1
- package/esm/relations/relation-loader.mjs +51 -16
- package/esm/relations/relation-loader.mjs.map +1 -1
- package/esm/remover/database-remover.d.mts.map +1 -1
- package/esm/remover/database-remover.mjs +21 -7
- package/esm/remover/database-remover.mjs.map +1 -1
- package/esm/restorer/database-restorer.d.mts +12 -2
- package/esm/restorer/database-restorer.d.mts.map +1 -1
- package/esm/restorer/database-restorer.mjs +43 -13
- package/esm/restorer/database-restorer.mjs.map +1 -1
- package/esm/sync/model-events.mjs +63 -3
- package/esm/sync/model-events.mjs.map +1 -1
- package/esm/sync/model-sync-operation.d.mts.map +1 -1
- package/esm/sync/model-sync-operation.mjs +3 -2
- package/esm/sync/model-sync-operation.mjs.map +1 -1
- package/esm/sync/sync-manager.d.mts +3 -42
- package/esm/sync/sync-manager.d.mts.map +1 -1
- package/esm/sync/sync-manager.mjs +37 -126
- package/esm/sync/sync-manager.mjs.map +1 -1
- package/esm/transactions/after-commit.d.mts +24 -0
- package/esm/transactions/after-commit.d.mts.map +1 -0
- package/esm/transactions/after-commit.mjs +47 -0
- package/esm/transactions/after-commit.mjs.map +1 -0
- package/esm/utils/connect-to-database.d.mts.map +1 -1
- package/esm/utils/connect-to-database.mjs +4 -4
- package/esm/utils/connect-to-database.mjs.map +1 -1
- package/esm/utils/database-writer.utils.d.mts +1 -1
- package/esm/utils/database-writer.utils.d.mts.map +1 -1
- package/esm/utils/database-writer.utils.mjs +1 -0
- package/esm/utils/database-writer.utils.mjs.map +1 -1
- package/esm/utils/define-model.d.mts.map +1 -1
- package/esm/utils/define-model.mjs +2 -10
- package/esm/utils/define-model.mjs.map +1 -1
- package/esm/utils/is-valid-date-value.mjs +3 -2
- package/esm/utils/is-valid-date-value.mjs.map +1 -1
- package/esm/utils/once-connected.d.mts.map +1 -1
- package/esm/utils/once-connected.mjs +12 -2
- package/esm/utils/once-connected.mjs.map +1 -1
- package/esm/validation/mutators/embed-mutator.mjs +19 -11
- package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
- package/esm/validation/plugins/embed-validator-plugin.d.mts +2 -2
- package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
- package/esm/validation/plugins/embed-validator-plugin.mjs +8 -2
- package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
- package/esm/validation/rules/database-model-rule.mjs +6 -5
- package/esm/validation/rules/database-model-rule.mjs.map +1 -1
- package/esm/validation/rules/exists-rule.mjs +6 -4
- package/esm/validation/rules/exists-rule.mjs.map +1 -1
- package/esm/validation/rules/unique-rule.mjs +5 -2
- package/esm/validation/rules/unique-rule.mjs.map +1 -1
- package/esm/validation/validators/embed-validator.d.mts.map +1 -1
- package/esm/validation/validators/embed-validator.mjs +1 -1
- package/esm/validation/validators/embed-validator.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts +27 -0
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +84 -17
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +2783 -2766
- package/llms.txt +15 -15
- package/package.json +6 -6
- package/skills/aggregate-data/SKILL.md +165 -165
- package/skills/alter-migration/SKILL.md +531 -531
- package/skills/configure-delete-strategy/SKILL.md +130 -130
- package/skills/define-model/SKILL.md +205 -205
- package/skills/define-relations/SKILL.md +171 -171
- package/skills/index.md +23 -0
- package/skills/manage-data-sources/SKILL.md +140 -140
- package/skills/manage-transactions/SKILL.md +208 -191
- package/skills/paginate-results/SKILL.md +122 -122
- package/skills/perform-atomic-ops/SKILL.md +156 -156
- package/skills/query-data/SKILL.md +213 -213
- package/skills/run-cascade-cli/SKILL.md +125 -125
- package/skills/search-by-vector/SKILL.md +127 -127
- package/skills/subscribe-to-model-events/SKILL.md +148 -148
- package/skills/track-changes/SKILL.md +127 -127
- package/skills/write-migration/SKILL.md +206 -206
- package/esm/drivers/postgres/postgres-sync-adapter.d.mts +0 -79
- package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +0 -1
- package/esm/drivers/postgres/postgres-sync-adapter.mjs +0 -162
- package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-restorer.mjs","names":[],"sources":["../../../../../../../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\";\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\n const finalData = await this.handleIdConflict(restoredData, id, onIdConflict);\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 });\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 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 const idExists = 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 } 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\n // Remove from trash (only for trash strategy)\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n const trashFilter = { [this.primaryKey]: id };\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 .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: Generates new ObjectId for `_id`, keeps `id` if it exists\n * For SQL: Removes `id` 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 isMongoDb = this.driver.name === \"mongodb\";\n const newData = { ...recordData };\n\n if (isMongoDb) {\n // MongoDB: Generate new ObjectId for _id\n if (this.primaryKey === \"_id\") {\n // Remove _id to let MongoDB generate new one\n delete newData._id;\n } else if (this.primaryKey === \"id\") {\n // Remove id to let ID generator create new one\n delete newData.id;\n }\n } else {\n // SQL: Remove primary key to let database auto-increment\n delete newData[this.primaryKey];\n }\n\n return newData;\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,mBAAb,MAA0D;;CAExD,AAAiB;;CAGjB,AAAiB;;CAGjB,AAAiB;;CAGjB,AAAiB;;CAGjB,AAAiB;;;;;;;;;;;;CAajB,AAAO,YAAY,YAA0B;EAC3C,KAAK,OAAO;EACZ,KAAK,aAAa,WAAW,cAAc;EAC3C,KAAK,SAAS,KAAK,WAAW;EAC9B,KAAK,QAAQ,WAAW;EACxB,KAAK,aAAa,WAAW;CAC/B;;;;;;;;;;CAWA,MAAa,QACX,IACA,UAA2B,CAAC,GACH;EACzB,MAAM,eAAe,QAAQ,gBAAgB;EAC7C,MAAM,aAAa,QAAQ,cAAc;EAGzC,MAAM,WAAW,KAAK,gBAAgB,QAAQ,QAAQ;EAGtD,IAAI,aAAa,aACf,MAAM,IAAI,MACR,kBAAkB,KAAK,KAAK,KAAK,QAAQ,KAAK,WAAW,GAAG,GAAG,kDACjE;EAIF,MAAM,aAAa,MAAM,KAAK,sBAAsB,IAAI,QAAQ;EAEhE,IAAI,CAAC,YACH,MAAM,IAAI,MACR,kBAAkB,KAAK,KAAK,KAAK,QAAQ,KAAK,WAAW,GAAG,GAAG,wBAAwB,aAAa,UAAU,gBAAgB,uBAAuB,EACvJ;EAIF,MAAM,eAAe,EAAE,GAAG,WAAW;EACrC,OAAO,aAAa;EACpB,OAAO,aAAa;EAGpB,MAAM,YAAY,MAAM,KAAK,iBAAiB,cAAc,IAAI,YAAY;EAI5E,MAAM,QAAQ,IAAK,KAAK,KAAa,SAAS;EAG9C,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,WAAW;EAInC,IAAI,aAAa,SAAS;GAExB,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,SAAS;GAC9C,MAAM,QAAQ;GAGd,MAAM,KAAK,OAAO,OAAO,KAAK,kBAAkB,GAAG,GAChD,KAAK,aAAa,GACrB,CAAC;EACH,OAAO,IAAI,aAAa,QAAQ;GAE9B,MAAM,kBAAkB,KAAK,KAAK,mBAAmB;GACrD,IAAI,iBAAiB;IACnB,MAAM,SAAS,GAAG,KAAK,aAAa,GAAG;IACvC,MAAM,mBAAqC,EACzC,QAAQ,GAAG,kBAAkB,EAAE,EACjC;IAEA,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,QAAQ,gBAAgB;IAC7D,MAAM,QAAQ;GAChB;EACF;EAGA,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,UAAU;EAGlC,OAAO;GACL,SAAS;GACT,eAAe;GACf;GACA,gBAAgB;EAClB;CACF;;;;;;;CAQA,MAAa,WAAW,UAA2B,CAAC,GAA4B;EAC9E,MAAM,eAAe,QAAQ,gBAAgB;EAC7C,MAAM,aAAa,QAAQ,cAAc;EAGzC,MAAM,WAAW,KAAK,gBAAgB,QAAQ,QAAQ;EAGtD,IAAI,aAAa,aACf,MAAM,IAAI,MACR,sBAAsB,KAAK,KAAK,KAAK,0DACvC;EAIF,MAAM,mBAAmB,MAAM,KAAK,0BAA0B,QAAQ;EAEtE,IAAI,iBAAiB,WAAW,GAC9B,OAAO;GACL,SAAS;GACT,eAAe;GACf;EACF;EAIF,IAAI,gBAAgB;EACpB,MAAM,YAA4D,CAAC;EAEnE,MAAM,kBAA2B,CAAC;EAElC,KAAK,MAAM,cAAc,kBAAkB;GACzC,MAAM,KAAK,WAAW,KAAK;GAE3B,IAAI;IAEF,MAAM,eAAe,EAAE,GAAG,WAAW;IACrC,OAAO,aAAa;IACpB,OAAO,aAAa;IAIpB,IAAI,MADmB,KAAK,cAAc,EAAE,GAC9B;KACZ,IAAI,iBAAiB,QACnB,MAAM,IAAI,MACR,kBAAkB,KAAK,KAAK,KAAK,QAAQ,KAAK,WAAW,GAAG,GAAG,qCACjE;KAIF,MAAM,YAAY,MAAM,KAAK,YAAY,YAAY;KACrD,UAAU,KAAK;MACb;MACA,QAAQ,MAAM,GAAG,mCAAmC,UAAU,KAAK;KACrE,CAAC;KAGD,MAAM,QAAQ,IAAK,KAAK,KAAa,SAAS;KAG9C,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,WAAW;KAInC,IAAI,aAAa,SAAS;MAExB,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,SAAS;MAC9C,MAAM,QAAQ;KAChB,OAAO,IAAI,aAAa,QAAQ;MAE9B,MAAM,kBAAkB,KAAK,KAAK,mBAAmB;MACrD,IAAI,iBAAiB;OACnB,MAAM,SAAS,GAAG,KAAK,aAAa,GAAG;OACvC,MAAM,mBAAqC,EACzC,QAAQ,GAAG,kBAAkB,EAAE,EACjC;OACA,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,QAAQ,gBAAgB;OAC7D,MAAM,QAAQ;MAChB;KACF;KAEA,gBAAgB,KAAK,KAAK;KAG1B,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,UAAU;IAEpC,OAAO;KAEL,MAAM,QAAQ,IAAK,KAAK,KAAa,YAAY;KAGjD,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,WAAW;KAInC,IAAI,aAAa,SAAS;MAExB,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,YAAY;MACjD,MAAM,QAAQ;KAChB,OAAO,IAAI,aAAa,QAAQ;MAE9B,MAAM,kBAAkB,KAAK,KAAK,mBAAmB;MACrD,IAAI,iBAAiB;OACnB,MAAM,SAAS,GAAG,KAAK,aAAa,GAAG;OACvC,MAAM,mBAAqC,EACzC,QAAQ,GAAG,kBAAkB,EAAE,EACjC;OACA,MAAM,KAAK,OAAO,OAAO,KAAK,OAAO,QAAQ,gBAAgB;OAC7D,MAAM,QAAQ;MAChB;KACF;KAEA,gBAAgB,KAAK,KAAK;KAG1B,IAAI,CAAC,YACH,MAAM,MAAM,UAAU,UAAU;IAEpC;IAGA,IAAI,aAAa,SAAS;KACxB,MAAM,aAAa,KAAK,kBAAkB;KAC1C,MAAM,cAAc,GAAG,KAAK,aAAa,GAAG;KAC5C,MAAM,KAAK,OAAO,OAAO,YAAY,WAAW;IAClD;IAEA;GACF,SAAS,OAAO;IACd,IAAI,iBAAiB,QACnB,MAAM;IAGR,UAAU,KAAK;KACb;KACA,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC/D,CAAC;GACH;EACF;EAEA,OAAO;GACL,SAAS;GACT;GACA;GACA;GACA,WAAW,UAAU,SAAS,IAAI,YAAY;EAChD;CACF;;;;;;;;;;CAWA,AAAQ,gBAAgB,gBAAmD;EACzE,OACE,kBACA,KAAK,KAAK,kBACV,KAAK,WAAW,yBAChB;CAEJ;;;;;;;;;CAUA,MAAc,sBAAsB,IAAqB,UAA4B;EACnF,IAAI,aAAa,SAAS;GACxB,MAAM,aAAa,KAAK,kBAAkB;GAC1C,IAAI;IAMF,OAAO,MALkB,KAAK,OAC3B,aAAa,UAAU,CAAC,CACxB,MAAM,KAAK,YAAY,EAAE,CAAC,CAC1B,MAA+B;GAGpC,QAAQ;IACN,OAAO;GACT;EACF,OAAO,IAAI,aAAa,QAAQ;GAC9B,MAAM,kBAAkB,KAAK,KAAK,mBAAmB;GACrD,IAAI,CAAC,iBACH,OAAO;GAGT,IAAI;IAOF,OAAO,MANwB,KAAK,OACjC,aAAa,KAAK,KAAK,CAAC,CACxB,MAAM,KAAK,YAAY,EAAE,CAAC,CAC1B,aAAa,eAAe,CAAC,CAC7B,MAA+B;GAGpC,QAAQ;IACN,OAAO;GACT;EACF;EAEA,OAAO;CACT;;;;;;;;CASA,MAAc,0BACZ,UACoC;EACpC,IAAI,aAAa,SAAS;GACxB,MAAM,aAAa,KAAK,kBAAkB;GAC1C,IAAI;IAMF,OAAQ,MALW,KAAK,OACrB,aAAa,UAAU,CAAC,CACxB,MAAM,iBAAiB,KAAK,KAAK,CAAC,CAClC,IAEoB;GACzB,QAAQ;IACN,OAAO,CAAC;GACV;EACF,OAAO,IAAI,aAAa,QAAQ;GAC9B,MAAM,kBAAkB,KAAK,KAAK,mBAAmB;GACrD,IAAI,CAAC,iBACH,OAAO,CAAC;GAGV,IAAI;IAMF,OAAQ,MALiB,KAAK,OAC3B,aAAa,KAAK,KAAK,CAAC,CACxB,aAAa,eAAe,CAAC,CAC7B,IAE0B;GAC/B,QAAQ;IACN,OAAO,CAAC;GACV;EACF;EAEA,OAAO,CAAC;CACV;;;;;;;;;;CAWA,MAAc,iBACZ,YACA,YACA,cACkC;EAGlC,IAAI,MAFmB,KAAK,cAAc,UAAU,GAEtC;GACZ,IAAI,iBAAiB,QACnB,MAAM,IAAI,MACR,kBAAkB,KAAK,KAAK,KAAK,QAAQ,KAAK,WAAW,GAAG,WAAW,qCACzE;GAIF,OAAO,MAAM,KAAK,YAAY,UAAU;EAC1C;EAEA,OAAO;CACT;;;;;;;;CASA,MAAc,cAAc,IAAuC;EACjE,IAAI;GAGF,OAAO,MAFO,KAAK,OAAO,aAAa,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC,CAAC,OAE7D;EACnB,QAAQ;GACN,OAAO;EACT;CACF;;;;;;;;;;;CAYA,MAAc,YAAY,YAAuE;EAC/F,MAAM,YAAY,KAAK,OAAO,SAAS;EACvC,MAAM,UAAU,EAAE,GAAG,WAAW;EAEhC,IAAI,WAEF;OAAI,KAAK,eAAe,OAEtB,OAAO,QAAQ;QACV,IAAI,KAAK,eAAe,MAE7B,OAAO,QAAQ;EACjB,OAGA,OAAO,QAAQ,KAAK;EAGtB,OAAO;CACT;;;;;;;;;;;;CAaA,AAAQ,oBAA4B;EAClC,IAAI,KAAK,KAAK,YACZ,OAAO,KAAK,KAAK;EAGnB,IAAI,KAAK,WAAW,mBAClB,OAAO,KAAK,WAAW;EAGzB,OAAO,GAAG,KAAK,MAAM;CACvB;AACF"}
|
|
1
|
+
{"version":3,"file":"database-restorer.mjs","names":[],"sources":["../../../../../../../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 { 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 * Re-run sync for a restored record so embedded copies come back up to date.\n * Failures are swallowed: the restore itself already succeeded.\n * TODO(5.22, K2:B10): move this fan-out to a retry/queue-backed path.\n *\n * @private\n */\n private async triggerSync(model: Model): Promise<void> {\n try {\n await triggerModelEvent(\n this.ctor as any,\n \"updated\",\n model,\n Object.keys(model.data as Record<string, unknown>),\n );\n } catch {\n // sync failures never fail a restore\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import events from "@mongez/events";
|
|
2
|
+
|
|
1
3
|
//#region ../cascade/src/sync/model-events.ts
|
|
2
4
|
/**
|
|
5
|
+
* Model sync event name helpers.
|
|
6
|
+
*
|
|
7
|
+
* Provides type-safe event names for model sync operations.
|
|
8
|
+
* Avoids hardcoded string literals throughout the codebase.
|
|
9
|
+
*
|
|
10
|
+
* @module cascade-next/sync/model-events
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
3
13
|
* Event name prefix for all model sync events.
|
|
4
14
|
*/
|
|
5
15
|
const MODEL_EVENT_PREFIX = "model";
|
|
@@ -11,6 +21,20 @@ const ModelSyncEventType = {
|
|
|
11
21
|
DELETED: "deleted"
|
|
12
22
|
};
|
|
13
23
|
/**
|
|
24
|
+
* Resolve the identity used in event names.
|
|
25
|
+
*
|
|
26
|
+
* Class names collide across packages (e.g. auth's `User` and an app `User`),
|
|
27
|
+
* so key on the table plus the data source when available and fall back to
|
|
28
|
+
* the class name only for models without a table.
|
|
29
|
+
*/
|
|
30
|
+
function getModelEventKey(modelClass) {
|
|
31
|
+
const table = modelClass.table;
|
|
32
|
+
if (!table) return modelClass.name;
|
|
33
|
+
const source = modelClass.dataSource;
|
|
34
|
+
const sourceName = typeof source === "string" ? source : source?.name;
|
|
35
|
+
return sourceName ? `${sourceName}:${table}` : table;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
14
38
|
* Get the event name for a model update.
|
|
15
39
|
*
|
|
16
40
|
* @param modelClass - The model class
|
|
@@ -23,7 +47,7 @@ const ModelSyncEventType = {
|
|
|
23
47
|
* ```
|
|
24
48
|
*/
|
|
25
49
|
function getModelUpdatedEvent(modelClass) {
|
|
26
|
-
return `${MODEL_EVENT_PREFIX}.${modelClass
|
|
50
|
+
return `${MODEL_EVENT_PREFIX}.${getModelEventKey(modelClass)}.${ModelSyncEventType.UPDATED}`;
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* Get the event name for a model deletion.
|
|
@@ -38,9 +62,45 @@ function getModelUpdatedEvent(modelClass) {
|
|
|
38
62
|
* ```
|
|
39
63
|
*/
|
|
40
64
|
function getModelDeletedEvent(modelClass) {
|
|
41
|
-
return `${MODEL_EVENT_PREFIX}.${modelClass
|
|
65
|
+
return `${MODEL_EVENT_PREFIX}.${getModelEventKey(modelClass)}.${ModelSyncEventType.DELETED}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the event name for a model by name string.
|
|
69
|
+
*
|
|
70
|
+
* @param modelName - The model class name
|
|
71
|
+
* @param eventType - The event type
|
|
72
|
+
* @returns Event name string
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const eventName = getModelEvent("Category", "updated");
|
|
77
|
+
* // Returns: "model.Category.updated"
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
function getModelEvent(modelName, eventType) {
|
|
81
|
+
return `${MODEL_EVENT_PREFIX}.${modelName}.${eventType}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Legacy event name for a model: `model.<ClassName>.<event>`.
|
|
85
|
+
*
|
|
86
|
+
* @deprecated Use the table-keyed name from `getModelUpdatedEvent` / `getModelDeletedEvent`.
|
|
87
|
+
* The class-name form is still emitted in 5.21 for compatibility and will be removed later.
|
|
88
|
+
*/
|
|
89
|
+
function getLegacyModelEvent(modelClass, eventType) {
|
|
90
|
+
return getModelEvent(modelClass.name, eventType);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Trigger a model sync event under both names: the deprecated class-name form first,
|
|
94
|
+
* then the table-keyed form. The second is skipped when both names are identical,
|
|
95
|
+
* so a listener on either form fires exactly once.
|
|
96
|
+
*/
|
|
97
|
+
async function triggerModelEvent(modelClass, eventType, ...args) {
|
|
98
|
+
const legacy = getLegacyModelEvent(modelClass, eventType);
|
|
99
|
+
const current = getModelEvent(getModelEventKey(modelClass), eventType);
|
|
100
|
+
await events.triggerAll(legacy, ...args);
|
|
101
|
+
if (current !== legacy) await events.triggerAll(current, ...args);
|
|
42
102
|
}
|
|
43
103
|
|
|
44
104
|
//#endregion
|
|
45
|
-
export { getModelDeletedEvent, getModelUpdatedEvent };
|
|
105
|
+
export { getModelDeletedEvent, getModelUpdatedEvent, triggerModelEvent };
|
|
46
106
|
//# sourceMappingURL=model-events.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-events.mjs","names":[],"sources":["../../../../../../../cascade/src/sync/model-events.ts"],"sourcesContent":["/**\n * Model sync event name helpers.\n *\n * Provides type-safe event names for model sync operations.\n * Avoids hardcoded string literals throughout the codebase.\n *\n * @module cascade-next/sync/model-events\n */\n\nimport type { ChildModel, Model } from \"../model/model\";\n\n/**\n * Event name prefix for all model sync events.\n */\nexport const MODEL_EVENT_PREFIX = \"model\";\n\n/**\n * Model sync event types.\n */\nexport const ModelSyncEventType = {\n UPDATED: \"updated\",\n DELETED: \"deleted\",\n} as const;\n\nexport type ModelSyncEventTypeName = (typeof ModelSyncEventType)[keyof typeof ModelSyncEventType];\n\n/**\n * Get the event name for a model update.\n *\n * @param modelClass - The model class\n * @returns Event name string (e.g., \"model.Category.updated\")\n *\n * @example\n * ```typescript\n * const eventName = getModelUpdatedEvent(Category);\n * // Returns: \"model.Category.updated\"\n * ```\n */\nexport function getModelUpdatedEvent(modelClass: ChildModel<Model>): string {\n return `${MODEL_EVENT_PREFIX}.${modelClass
|
|
1
|
+
{"version":3,"file":"model-events.mjs","names":[],"sources":["../../../../../../../cascade/src/sync/model-events.ts"],"sourcesContent":["/**\n * Model sync event name helpers.\n *\n * Provides type-safe event names for model sync operations.\n * Avoids hardcoded string literals throughout the codebase.\n *\n * @module cascade-next/sync/model-events\n */\n\nimport events from \"@mongez/events\";\nimport type { ChildModel, Model } from \"../model/model\";\n\n/**\n * Event name prefix for all model sync events.\n */\nexport const MODEL_EVENT_PREFIX = \"model\";\n\n/**\n * Model sync event types.\n */\nexport const ModelSyncEventType = {\n UPDATED: \"updated\",\n DELETED: \"deleted\",\n} as const;\n\nexport type ModelSyncEventTypeName = (typeof ModelSyncEventType)[keyof typeof ModelSyncEventType];\n\n/**\n * Resolve the identity used in event names.\n *\n * Class names collide across packages (e.g. auth's `User` and an app `User`),\n * so key on the table plus the data source when available and fall back to\n * the class name only for models without a table.\n */\nfunction getModelEventKey(modelClass: ChildModel<Model>): string {\n const table = modelClass.table;\n\n if (!table) {\n return modelClass.name;\n }\n\n const source = modelClass.dataSource;\n const sourceName = typeof source === \"string\" ? source : source?.name;\n\n return sourceName ? `${sourceName}:${table}` : table;\n}\n\n/**\n * Get the event name for a model update.\n *\n * @param modelClass - The model class\n * @returns Event name string (e.g., \"model.Category.updated\")\n *\n * @example\n * ```typescript\n * const eventName = getModelUpdatedEvent(Category);\n * // Returns: \"model.Category.updated\"\n * ```\n */\nexport function getModelUpdatedEvent(modelClass: ChildModel<Model>): string {\n return `${MODEL_EVENT_PREFIX}.${getModelEventKey(modelClass)}.${ModelSyncEventType.UPDATED}`;\n}\n\n/**\n * Get the event name for a model deletion.\n *\n * @param modelClass - The model class\n * @returns Event name string (e.g., \"model.Category.deleted\")\n *\n * @example\n * ```typescript\n * const eventName = getModelDeletedEvent(Category);\n * // Returns: \"model.Category.deleted\"\n * ```\n */\nexport function getModelDeletedEvent(modelClass: ChildModel<Model>): string {\n return `${MODEL_EVENT_PREFIX}.${getModelEventKey(modelClass)}.${ModelSyncEventType.DELETED}`;\n}\n\n/**\n * Get the event name for a model by name string.\n *\n * @param modelName - The model class name\n * @param eventType - The event type\n * @returns Event name string\n *\n * @example\n * ```typescript\n * const eventName = getModelEvent(\"Category\", \"updated\");\n * // Returns: \"model.Category.updated\"\n * ```\n */\nexport function getModelEvent(modelName: string, eventType: ModelSyncEventTypeName): string {\n return `${MODEL_EVENT_PREFIX}.${modelName}.${eventType}`;\n}\n\n/**\n * Legacy event name for a model: `model.<ClassName>.<event>`.\n *\n * @deprecated Use the table-keyed name from `getModelUpdatedEvent` / `getModelDeletedEvent`.\n * The class-name form is still emitted in 5.21 for compatibility and will be removed later.\n */\nexport function getLegacyModelEvent(\n modelClass: ChildModel<Model>,\n eventType: ModelSyncEventTypeName,\n): string {\n return getModelEvent(modelClass.name, eventType);\n}\n\n/**\n * Trigger a model sync event under both names: the deprecated class-name form first,\n * then the table-keyed form. The second is skipped when both names are identical,\n * so a listener on either form fires exactly once.\n */\nexport async function triggerModelEvent(\n modelClass: ChildModel<Model>,\n eventType: ModelSyncEventTypeName,\n ...args: unknown[]\n): Promise<void> {\n const legacy = getLegacyModelEvent(modelClass, eventType);\n const current = getModelEvent(getModelEventKey(modelClass), eventType);\n\n // Deprecated class-name form, kept for 5.21 compatibility.\n await events.triggerAll(legacy, ...args);\n\n if (current !== legacy) {\n await events.triggerAll(current, ...args);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAa,qBAAqB;;;;AAKlC,MAAa,qBAAqB;CAChC,SAAS;CACT,SAAS;AACX;;;;;;;;AAWA,SAAS,iBAAiB,YAAuC;CAC/D,MAAM,QAAQ,WAAW;CAEzB,IAAI,CAAC,OACH,OAAO,WAAW;CAGpB,MAAM,SAAS,WAAW;CAC1B,MAAM,aAAa,OAAO,WAAW,WAAW,SAAS,QAAQ;CAEjE,OAAO,aAAa,GAAG,WAAW,GAAG,UAAU;AACjD;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,YAAuC;CAC1E,OAAO,GAAG,mBAAmB,GAAG,iBAAiB,UAAU,EAAE,GAAG,mBAAmB;AACrF;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,YAAuC;CAC1E,OAAO,GAAG,mBAAmB,GAAG,iBAAiB,UAAU,EAAE,GAAG,mBAAmB;AACrF;;;;;;;;;;;;;;AAeA,SAAgB,cAAc,WAAmB,WAA2C;CAC1F,OAAO,GAAG,mBAAmB,GAAG,UAAU,GAAG;AAC/C;;;;;;;AAQA,SAAgB,oBACd,YACA,WACQ;CACR,OAAO,cAAc,WAAW,MAAM,SAAS;AACjD;;;;;;AAOA,eAAsB,kBACpB,YACA,WACA,GAAG,MACY;CACf,MAAM,SAAS,oBAAoB,YAAY,SAAS;CACxD,MAAM,UAAU,cAAc,iBAAiB,UAAU,GAAG,SAAS;CAGrE,MAAM,OAAO,WAAW,QAAQ,GAAG,IAAI;CAEvC,IAAI,YAAY,QACd,MAAM,OAAO,WAAW,SAAS,GAAG,IAAI;AAE5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-sync-operation.d.mts","names":[],"sources":["../../../../../../../cascade/src/sync/model-sync-operation.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"model-sync-operation.d.mts","names":[],"sources":["../../../../../../../cascade/src/sync/model-sync-operation.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;cAkCa,kBAAA,YAA8B,0BAAA;EA2BvC;;;EAAA,iBAvBe,MAAA;EA0EV;;;EAAA,iBArEU,aAAA;EAsGV;;;EAAA,QAjGC,YAAA;EA4IA;;;;;;;;cAjIN,gBAAA,EAAkB,UAAA,CAAW,KAAA,GAC7B,gBAAA,EAAkB,UAAA,CAAW,KAAA,GAC7B,WAAA,UACA,MAAA;EAoSkB;;;AAAwB;;;;;;;;EAnQrC,KAAA,CAAM,MAAA;;;;;;;;;;;;;EAiBN,UAAA,CAAW,KAAA;;;;;;;;;;;;EAgBX,QAAA,CAAS,KAAA;;;;;;;;;;;;;EAiBT,WAAA,CAAY,MAAA;;;;;;;;;;;EAeZ,aAAA;;;;;;;;;;;EAeA,cAAA;;;;;UAaC,SAAA;;;;;EAwBD,WAAA;;;;EAYA,QAAA;;;;;UAYC,kBAAA;;;;;UAuCA,kBAAA;;;;UAwCA,UAAA;;;;UAWA,eAAA;;;;UAiBM,qBAAA;;;;EAWP,SAAA,IAAa,QAAA,CAAS,eAAA;AAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getModelDeletedEvent, getModelUpdatedEvent } from "./model-events.mjs";
|
|
2
2
|
import { DEFAULT_MAX_SYNC_DEPTH } from "./sync-context.mjs";
|
|
3
3
|
import { SyncManager } from "./sync-manager.mjs";
|
|
4
|
+
import { log } from "@warlock.js/logger";
|
|
4
5
|
import events from "@mongez/events";
|
|
5
6
|
|
|
6
7
|
//#region ../cascade/src/sync/model-sync-operation.ts
|
|
@@ -194,7 +195,7 @@ var ModelSyncOperation = class {
|
|
|
194
195
|
const driver = this.config.sourceModel.getDataSource().driver;
|
|
195
196
|
await new SyncManager(this.config.sourceModel, driver).syncUpdateWithConfig(sourceId, model, changedFields, this.buildSyncConfig());
|
|
196
197
|
} catch (error) {
|
|
197
|
-
|
|
198
|
+
log.error("database", "sync.failed", `[ModelSync] Failed to sync ${this.config.sourceModel.name} -> ${this.config.targetModel.name}: ${error}`);
|
|
198
199
|
}
|
|
199
200
|
};
|
|
200
201
|
/**
|
|
@@ -211,7 +212,7 @@ var ModelSyncOperation = class {
|
|
|
211
212
|
if (this.config.removeOnDelete) await this.removeTargetDocuments(sourceId, driver);
|
|
212
213
|
else if (this.config.unsetOnDelete) await new SyncManager(this.config.sourceModel, driver).syncDeleteWithConfig(sourceId, this.buildSyncConfig());
|
|
213
214
|
} catch (error) {
|
|
214
|
-
|
|
215
|
+
log.error("database", "sync.failed", `[ModelSync] Failed to handle delete for ${this.config.sourceModel.name}: ${error}`);
|
|
215
216
|
}
|
|
216
217
|
};
|
|
217
218
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-sync-operation.mjs","names":[],"sources":["../../../../../../../cascade/src/sync/model-sync-operation.ts"],"sourcesContent":["/**\n * Model sync operation class.\n *\n * Manages a single sync relationship between a source and target model.\n * Subscribes to source model events and triggers sync operations when\n * the source is updated or deleted.\n *\n * @module cascade-next/sync/model-sync-operation\n */\n\nimport events, { type EventSubscription } from \"@mongez/events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelDeletedEvent, getModelUpdatedEvent } from \"./model-events\";\nimport { DEFAULT_MAX_SYNC_DEPTH } from \"./sync-context\";\nimport { SyncManager } from \"./sync-manager\";\nimport type { ModelSyncConfig, ModelSyncOperationContract } from \"./types\";\n\n/**\n * Manages a single model sync operation.\n *\n * Subscribes to source model events (updated, deleted) and triggers\n * sync operations to update embedded data in target models.\n *\n * @example\n * ```typescript\n * // Created via modelSync.sync() or Model.sync()\n * const operation = new ModelSyncOperation(Category, Product, \"category\", false);\n * operation\n * .embed(\"embedMinimal\")\n * .watchFields([\"name\", \"slug\"])\n * .unsetOnDelete();\n * ```\n */\nexport class ModelSyncOperation implements ModelSyncOperationContract {\n /**\n * Configuration for this sync operation.\n */\n private readonly config: ModelSyncConfig;\n\n /**\n * Active event subscriptions for cleanup.\n */\n private readonly subscriptions: EventSubscription[] = [];\n\n /**\n * Whether this operation is currently subscribed to events.\n */\n private isSubscribed = false;\n\n /**\n * Create a new model sync operation.\n *\n * @param sourceModel - Source model class that triggers sync\n * @param targetModel - Target model class that receives data\n * @param targetField - Field path in target model\n * @param isMany - Whether this syncs to an array field\n */\n public constructor(\n sourceModelClass: ChildModel<Model>,\n targetModelClass: ChildModel<Model>,\n targetField: string,\n isMany: boolean,\n ) {\n this.config = {\n sourceModel: sourceModelClass,\n targetModel: targetModelClass,\n targetField,\n isMany,\n embedKey: \"embedData\",\n identifierField: \"id\",\n maxSyncDepth: DEFAULT_MAX_SYNC_DEPTH,\n watchFields: [],\n unsetOnDelete: false,\n removeOnDelete: false,\n };\n\n this.subscribe();\n }\n\n // ============================================================================\n // FLUENT CONFIGURATION\n // ============================================================================\n\n /**\n * Set the embed method to call on source model.\n *\n * @param embed - getter property name (e.g., \"embedData\", \"embedMinimal\") Or Array of fields\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").embed(\"embedMinimal\");\n * ```\n */\n public embed(method: string | string[]): this {\n this.config.embedKey = method;\n return this;\n }\n\n /**\n * Set the identifier field for array matching.\n * Required when syncing to array fields (syncMany).\n *\n * @param field - Field name used as identifier (default: \"id\")\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Tag.syncMany(Post, \"tags\").identifyBy(\"tagId\");\n * ```\n */\n public identifyBy(field: string): this {\n this.config.identifierField = field;\n return this;\n }\n\n /**\n * Set the maximum sync depth for chained operations.\n *\n * @param depth - Maximum depth (default: 3)\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").maxDepth(2);\n * ```\n */\n public maxDepth(depth: number): this {\n this.config.maxSyncDepth = depth;\n return this;\n }\n\n /**\n * Set which fields to watch for changes.\n * Sync only triggers when these fields change.\n *\n * @param fields - Array of field names to watch (empty = all)\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").watchFields([\"name\", \"slug\"]);\n * ```\n */\n public watchFields(fields: string[]): this {\n this.config.watchFields = fields;\n return this;\n }\n\n /**\n * Unset the target field when source is deleted.\n *\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").unsetOnDelete();\n * ```\n */\n public unsetOnDelete(): this {\n this.config.unsetOnDelete = true;\n return this;\n }\n\n /**\n * Delete target documents when source is deleted.\n *\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * User.sync(Profile, \"user\").removeOnDelete();\n * ```\n */\n public removeOnDelete(): this {\n this.config.removeOnDelete = true;\n return this;\n }\n\n // ============================================================================\n // LIFECYCLE\n // ============================================================================\n\n /**\n * Subscribe to source model events.\n * Called automatically in constructor.\n */\n private subscribe(): void {\n if (this.isSubscribed) {\n return;\n }\n\n const sourceModel = this.config.sourceModel;\n\n // Subscribe to model.updated event\n this.subscriptions.push(\n events.subscribe(getModelUpdatedEvent(sourceModel), this.handleModelUpdated),\n );\n\n // Subscribe to model.deleted event\n this.subscriptions.push(\n events.subscribe(getModelDeletedEvent(sourceModel), this.handleModelDeleted),\n );\n\n this.isSubscribed = true;\n }\n\n /**\n * Unsubscribe from all events and cleanup.\n * Called automatically when using modelSync.register().\n */\n public unsubscribe(): void {\n for (const subscription of this.subscriptions) {\n subscription.unsubscribe?.();\n }\n\n this.subscriptions.length = 0;\n this.isSubscribed = false;\n }\n\n /**\n * Cleanup the sync operations\n */\n public $cleanup() {\n return this.unsubscribe();\n }\n\n // ============================================================================\n // EVENT HANDLERS\n // ============================================================================\n\n /**\n * Handle model updated event.\n * Triggers sync to update embedded data in target models.\n */\n private handleModelUpdated = async (model: Model, changedFields: string[]): Promise<void> => {\n // Check if we should sync based on watched fields\n if (!this.shouldSync(changedFields)) {\n return;\n }\n\n try {\n const primaryKey = (this.config.sourceModel as any).primaryKey || \"id\";\n const sourceId = model.get(primaryKey) as string | number;\n\n if (!sourceId) {\n return;\n }\n\n // Get the driver from the source model's data source\n const dataSource = this.config.sourceModel.getDataSource();\n const driver = dataSource.driver;\n\n // Create sync manager and execute\n const syncManager = new SyncManager(this.config.sourceModel, driver);\n await syncManager.syncUpdateWithConfig(\n sourceId,\n model,\n changedFields,\n this.buildSyncConfig(),\n );\n } catch (error) {\n console.error(\n `[ModelSync] Failed to sync ${this.config.sourceModel.name} -> ${this.config.targetModel.name}:`,\n error,\n );\n }\n };\n\n /**\n * Handle model deleted event.\n * Triggers unset or remove based on configuration.\n */\n private handleModelDeleted = async (model: Model): Promise<void> => {\n if (!this.config.unsetOnDelete && !this.config.removeOnDelete) {\n return;\n }\n\n try {\n const primaryKey = (this.config.sourceModel as any).primaryKey || \"id\";\n const sourceId = model.get(primaryKey) as string | number;\n\n if (!sourceId) {\n return;\n }\n\n const dataSource = this.config.sourceModel.getDataSource();\n const driver = dataSource.driver;\n\n if (this.config.removeOnDelete) {\n // Delete all target documents that reference this source\n await this.removeTargetDocuments(sourceId, driver);\n } else if (this.config.unsetOnDelete) {\n // Unset the field in target documents\n const syncManager = new SyncManager(this.config.sourceModel, driver);\n await syncManager.syncDeleteWithConfig(sourceId, this.buildSyncConfig());\n }\n } catch (error) {\n console.error(\n `[ModelSync] Failed to handle delete for ${this.config.sourceModel.name}:`,\n error,\n );\n }\n };\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Check if sync should proceed based on watched fields.\n */\n private shouldSync(changedFields: string[]): boolean {\n if (this.config.watchFields.length === 0) {\n return true; // Watch all fields\n }\n\n return this.config.watchFields.some((field) => changedFields.includes(field));\n }\n\n /**\n * Build sync config compatible with SyncManager.\n */\n private buildSyncConfig(): any {\n return {\n targetField: this.config.targetField,\n isMany: this.config.isMany,\n embedKey: this.config.embedKey,\n identifierField: this.config.identifierField,\n maxSyncDepth: this.config.maxSyncDepth,\n preventCircularSync: true,\n watchFields: this.config.watchFields,\n unsetOnDelete: this.config.unsetOnDelete,\n targetModelClass: this.config.targetModel,\n };\n }\n\n /**\n * Remove target documents that reference the deleted source.\n */\n private async removeTargetDocuments(sourceId: string | number, driver: any): Promise<void> {\n const filter = this.config.isMany\n ? { [`${this.config.targetField}.${this.config.identifierField}`]: sourceId }\n : { [`${this.config.targetField}.${this.config.identifierField}`]: sourceId };\n\n await driver.deleteMany(this.config.targetModel.table, filter);\n }\n\n /**\n * Get the current configuration (for debugging/testing).\n */\n public getConfig(): Readonly<ModelSyncConfig> {\n return { ...this.config };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"model-sync-operation.mjs","names":[],"sources":["../../../../../../../cascade/src/sync/model-sync-operation.ts"],"sourcesContent":["/**\n * Model sync operation class.\n *\n * Manages a single sync relationship between a source and target model.\n * Subscribes to source model events and triggers sync operations when\n * the source is updated or deleted.\n *\n * @module cascade-next/sync/model-sync-operation\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport events, { type EventSubscription } from \"@mongez/events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelDeletedEvent, getModelUpdatedEvent } from \"./model-events\";\nimport { DEFAULT_MAX_SYNC_DEPTH } from \"./sync-context\";\nimport { SyncManager } from \"./sync-manager\";\nimport type { ModelSyncConfig, ModelSyncOperationContract } from \"./types\";\n\n/**\n * Manages a single model sync operation.\n *\n * Subscribes to source model events (updated, deleted) and triggers\n * sync operations to update embedded data in target models.\n *\n * @example\n * ```typescript\n * // Created via modelSync.sync() or Model.sync()\n * const operation = new ModelSyncOperation(Category, Product, \"category\", false);\n * operation\n * .embed(\"embedMinimal\")\n * .watchFields([\"name\", \"slug\"])\n * .unsetOnDelete();\n * ```\n */\nexport class ModelSyncOperation implements ModelSyncOperationContract {\n /**\n * Configuration for this sync operation.\n */\n private readonly config: ModelSyncConfig;\n\n /**\n * Active event subscriptions for cleanup.\n */\n private readonly subscriptions: EventSubscription[] = [];\n\n /**\n * Whether this operation is currently subscribed to events.\n */\n private isSubscribed = false;\n\n /**\n * Create a new model sync operation.\n *\n * @param sourceModel - Source model class that triggers sync\n * @param targetModel - Target model class that receives data\n * @param targetField - Field path in target model\n * @param isMany - Whether this syncs to an array field\n */\n public constructor(\n sourceModelClass: ChildModel<Model>,\n targetModelClass: ChildModel<Model>,\n targetField: string,\n isMany: boolean,\n ) {\n this.config = {\n sourceModel: sourceModelClass,\n targetModel: targetModelClass,\n targetField,\n isMany,\n embedKey: \"embedData\",\n identifierField: \"id\",\n maxSyncDepth: DEFAULT_MAX_SYNC_DEPTH,\n watchFields: [],\n unsetOnDelete: false,\n removeOnDelete: false,\n };\n\n this.subscribe();\n }\n\n // ============================================================================\n // FLUENT CONFIGURATION\n // ============================================================================\n\n /**\n * Set the embed method to call on source model.\n *\n * @param embed - getter property name (e.g., \"embedData\", \"embedMinimal\") Or Array of fields\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").embed(\"embedMinimal\");\n * ```\n */\n public embed(method: string | string[]): this {\n this.config.embedKey = method;\n return this;\n }\n\n /**\n * Set the identifier field for array matching.\n * Required when syncing to array fields (syncMany).\n *\n * @param field - Field name used as identifier (default: \"id\")\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Tag.syncMany(Post, \"tags\").identifyBy(\"tagId\");\n * ```\n */\n public identifyBy(field: string): this {\n this.config.identifierField = field;\n return this;\n }\n\n /**\n * Set the maximum sync depth for chained operations.\n *\n * @param depth - Maximum depth (default: 3)\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").maxDepth(2);\n * ```\n */\n public maxDepth(depth: number): this {\n this.config.maxSyncDepth = depth;\n return this;\n }\n\n /**\n * Set which fields to watch for changes.\n * Sync only triggers when these fields change.\n *\n * @param fields - Array of field names to watch (empty = all)\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").watchFields([\"name\", \"slug\"]);\n * ```\n */\n public watchFields(fields: string[]): this {\n this.config.watchFields = fields;\n return this;\n }\n\n /**\n * Unset the target field when source is deleted.\n *\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * Category.sync(Product, \"category\").unsetOnDelete();\n * ```\n */\n public unsetOnDelete(): this {\n this.config.unsetOnDelete = true;\n return this;\n }\n\n /**\n * Delete target documents when source is deleted.\n *\n * @returns This operation for chaining\n *\n * @example\n * ```typescript\n * User.sync(Profile, \"user\").removeOnDelete();\n * ```\n */\n public removeOnDelete(): this {\n this.config.removeOnDelete = true;\n return this;\n }\n\n // ============================================================================\n // LIFECYCLE\n // ============================================================================\n\n /**\n * Subscribe to source model events.\n * Called automatically in constructor.\n */\n private subscribe(): void {\n if (this.isSubscribed) {\n return;\n }\n\n const sourceModel = this.config.sourceModel;\n\n // Subscribe to model.updated event\n this.subscriptions.push(\n events.subscribe(getModelUpdatedEvent(sourceModel), this.handleModelUpdated),\n );\n\n // Subscribe to model.deleted event\n this.subscriptions.push(\n events.subscribe(getModelDeletedEvent(sourceModel), this.handleModelDeleted),\n );\n\n this.isSubscribed = true;\n }\n\n /**\n * Unsubscribe from all events and cleanup.\n * Called automatically when using modelSync.register().\n */\n public unsubscribe(): void {\n for (const subscription of this.subscriptions) {\n subscription.unsubscribe?.();\n }\n\n this.subscriptions.length = 0;\n this.isSubscribed = false;\n }\n\n /**\n * Cleanup the sync operations\n */\n public $cleanup() {\n return this.unsubscribe();\n }\n\n // ============================================================================\n // EVENT HANDLERS\n // ============================================================================\n\n /**\n * Handle model updated event.\n * Triggers sync to update embedded data in target models.\n */\n private handleModelUpdated = async (model: Model, changedFields: string[]): Promise<void> => {\n // Check if we should sync based on watched fields\n if (!this.shouldSync(changedFields)) {\n return;\n }\n\n try {\n const primaryKey = (this.config.sourceModel as any).primaryKey || \"id\";\n const sourceId = model.get(primaryKey) as string | number;\n\n if (!sourceId) {\n return;\n }\n\n // Get the driver from the source model's data source\n const dataSource = this.config.sourceModel.getDataSource();\n const driver = dataSource.driver;\n\n // Create sync manager and execute\n const syncManager = new SyncManager(this.config.sourceModel, driver);\n await syncManager.syncUpdateWithConfig(\n sourceId,\n model,\n changedFields,\n this.buildSyncConfig(),\n );\n } catch (error) {\n log.error(\n \"database\",\n \"sync.failed\",\n `[ModelSync] Failed to sync ${this.config.sourceModel.name} -> ${this.config.targetModel.name}: ${error}`,\n );\n }\n };\n\n /**\n * Handle model deleted event.\n * Triggers unset or remove based on configuration.\n */\n private handleModelDeleted = async (model: Model): Promise<void> => {\n if (!this.config.unsetOnDelete && !this.config.removeOnDelete) {\n return;\n }\n\n try {\n const primaryKey = (this.config.sourceModel as any).primaryKey || \"id\";\n const sourceId = model.get(primaryKey) as string | number;\n\n if (!sourceId) {\n return;\n }\n\n const dataSource = this.config.sourceModel.getDataSource();\n const driver = dataSource.driver;\n\n if (this.config.removeOnDelete) {\n // Delete all target documents that reference this source\n await this.removeTargetDocuments(sourceId, driver);\n } else if (this.config.unsetOnDelete) {\n // Unset the field in target documents\n const syncManager = new SyncManager(this.config.sourceModel, driver);\n await syncManager.syncDeleteWithConfig(sourceId, this.buildSyncConfig());\n }\n } catch (error) {\n log.error(\n \"database\",\n \"sync.failed\",\n `[ModelSync] Failed to handle delete for ${this.config.sourceModel.name}: ${error}`,\n );\n }\n };\n\n // ============================================================================\n // HELPERS\n // ============================================================================\n\n /**\n * Check if sync should proceed based on watched fields.\n */\n private shouldSync(changedFields: string[]): boolean {\n if (this.config.watchFields.length === 0) {\n return true; // Watch all fields\n }\n\n return this.config.watchFields.some((field) => changedFields.includes(field));\n }\n\n /**\n * Build sync config compatible with SyncManager.\n */\n private buildSyncConfig(): any {\n return {\n targetField: this.config.targetField,\n isMany: this.config.isMany,\n embedKey: this.config.embedKey,\n identifierField: this.config.identifierField,\n maxSyncDepth: this.config.maxSyncDepth,\n preventCircularSync: true,\n watchFields: this.config.watchFields,\n unsetOnDelete: this.config.unsetOnDelete,\n targetModelClass: this.config.targetModel,\n };\n }\n\n /**\n * Remove target documents that reference the deleted source.\n */\n private async removeTargetDocuments(sourceId: string | number, driver: any): Promise<void> {\n const filter = this.config.isMany\n ? { [`${this.config.targetField}.${this.config.identifierField}`]: sourceId }\n : { [`${this.config.targetField}.${this.config.identifierField}`]: sourceId };\n\n await driver.deleteMany(this.config.targetModel.table, filter);\n }\n\n /**\n * Get the current configuration (for debugging/testing).\n */\n public getConfig(): Readonly<ModelSyncConfig> {\n return { ...this.config };\n }\n}\n"],"mappings":"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"}
|
|
@@ -69,17 +69,6 @@ declare class SyncManager {
|
|
|
69
69
|
* @returns Array of sync instructions
|
|
70
70
|
*/
|
|
71
71
|
private collectDeleteInstructions;
|
|
72
|
-
/**
|
|
73
|
-
* Collects instructions for the next level in the sync chain.
|
|
74
|
-
*
|
|
75
|
-
* @param parentInstruction - The parent instruction
|
|
76
|
-
* @param embedData - Embedded data from parent
|
|
77
|
-
* @param changedFields - Changed fields
|
|
78
|
-
* @param parentConfig - Parent sync config
|
|
79
|
-
* @param parentOptions - Parent instruction options
|
|
80
|
-
* @returns Array of next-level sync instructions
|
|
81
|
-
*/
|
|
82
|
-
private collectNextLevelInstructions;
|
|
83
72
|
/**
|
|
84
73
|
* Builds an update sync instruction.
|
|
85
74
|
*
|
|
@@ -116,28 +105,14 @@ declare class SyncManager {
|
|
|
116
105
|
*/
|
|
117
106
|
private buildUpdate;
|
|
118
107
|
/**
|
|
119
|
-
* Executes sync instructions
|
|
120
|
-
*
|
|
108
|
+
* Executes sync instructions, once each, through the driver's dialect-aware
|
|
109
|
+
* sync adapter. Instructions run in depth order and each one records its own
|
|
110
|
+
* outcome, so one failing instruction never re-runs the ones that succeeded.
|
|
121
111
|
*
|
|
122
112
|
* @param instructions - Array of sync instructions
|
|
123
113
|
* @returns Sync result
|
|
124
114
|
*/
|
|
125
115
|
private executeInstructions;
|
|
126
|
-
/**
|
|
127
|
-
* Executes instructions in batch.
|
|
128
|
-
*
|
|
129
|
-
* @param instructions - Instructions to execute
|
|
130
|
-
* @param result - Result object to update
|
|
131
|
-
*/
|
|
132
|
-
private executeBatch;
|
|
133
|
-
/**
|
|
134
|
-
* Executes instructions individually (fallback).
|
|
135
|
-
* Provides detailed error reporting for each failed instruction.
|
|
136
|
-
*
|
|
137
|
-
* @param instructions - Instructions to execute
|
|
138
|
-
* @param result - Result object to update
|
|
139
|
-
*/
|
|
140
|
-
private executeIndividual;
|
|
141
116
|
/**
|
|
142
117
|
* Formats a sync error with detailed context.
|
|
143
118
|
*
|
|
@@ -153,13 +128,6 @@ declare class SyncManager {
|
|
|
153
128
|
* @returns Map of depth to instructions (sorted ascending)
|
|
154
129
|
*/
|
|
155
130
|
private groupByDepth;
|
|
156
|
-
/**
|
|
157
|
-
* Groups instructions by target table for batch optimization.
|
|
158
|
-
*
|
|
159
|
-
* @param instructions - Instructions to group
|
|
160
|
-
* @returns Map of table name to instructions
|
|
161
|
-
*/
|
|
162
|
-
private groupByTable;
|
|
163
131
|
/**
|
|
164
132
|
* Checks if sync should proceed based on watched fields.
|
|
165
133
|
*
|
|
@@ -182,13 +150,6 @@ declare class SyncManager {
|
|
|
182
150
|
* @returns Array of sync configurations
|
|
183
151
|
*/
|
|
184
152
|
private getSyncConfigs;
|
|
185
|
-
/**
|
|
186
|
-
* Gets sync configurations for a specific model.
|
|
187
|
-
*
|
|
188
|
-
* @param modelClass - The model class
|
|
189
|
-
* @returns Array of sync configurations
|
|
190
|
-
*/
|
|
191
|
-
private getSyncConfigsForModel;
|
|
192
153
|
/**
|
|
193
154
|
* Emits a syncing event.
|
|
194
155
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-manager.d.mts","names":[],"sources":["../../../../../../../cascade/src/sync/sync-manager.ts"],"mappings":";;;;;;;;;cAqBa,WAAA;EAAA,iBACM,WAAA;EAAA,iBACA,MAAA;EAyBN;;;;;;cAjBQ,WAAA,EAAa,UAAA,CAAW,KAAA,GAAQ,MAAA,EAAQ,cAAA;EAkJjD;;;;;;;;EArIG,UAAA,CACX,QAAA,mBACA,WAAA,EAAa,MAAA,oBAA0B,KAAA,EACvC,aAAA,aACC,OAAA,CAAQ,UAAA;;;;;;;;;;;EAgEE,oBAAA,CACX,QAAA,mBACA,WAAA,EAAa,MAAA,oBAA0B,KAAA,EACvC,aAAA,YACA,MAAA,EAAQ,UAAA,GACP,OAAA,CAAQ,UAAA;EAtET;;;;;;;;EAgIW,oBAAA,CACX,QAAA,mBACA,MAAA,EAAQ,UAAA,GACP,OAAA,CAAQ,UAAA;EA9DD;;;;;;EAoHG,UAAA,CAAW,QAAA,oBAA4B,OAAA,CAAQ,UAAA;EAvD1D;;;;;;EAAA,QA6GY,mBAAA;EAAA;;;;;;;;EAAA,
|
|
1
|
+
{"version":3,"file":"sync-manager.d.mts","names":[],"sources":["../../../../../../../cascade/src/sync/sync-manager.ts"],"mappings":";;;;;;;;;cAqBa,WAAA;EAAA,iBACM,WAAA;EAAA,iBACA,MAAA;EAyBN;;;;;;cAjBQ,WAAA,EAAa,UAAA,CAAW,KAAA,GAAQ,MAAA,EAAQ,cAAA;EAkJjD;;;;;;;;EArIG,UAAA,CACX,QAAA,mBACA,WAAA,EAAa,MAAA,oBAA0B,KAAA,EACvC,aAAA,aACC,OAAA,CAAQ,UAAA;;;;;;;;;;;EAgEE,oBAAA,CACX,QAAA,mBACA,WAAA,EAAa,MAAA,oBAA0B,KAAA,EACvC,aAAA,YACA,MAAA,EAAQ,UAAA,GACP,OAAA,CAAQ,UAAA;EAtET;;;;;;;;EAgIW,oBAAA,CACX,QAAA,mBACA,MAAA,EAAQ,UAAA,GACP,OAAA,CAAQ,UAAA;EA9DD;;;;;;EAoHG,UAAA,CAAW,QAAA,oBAA4B,OAAA,CAAQ,UAAA;EAvD1D;;;;;;EAAA,QA6GY,mBAAA;EAAA;;;;;;;;EAAA,QAkDA,yBAAA;EAmQN;;;;;;;AAkHiB;;EAlHjB,QAzNA,sBAAA;;;;;;;;;UAwCA,sBAAA;;;;;;;;UA+BA,WAAA;;;;;;;;UAiBA,WAAA;;;;;;;;;UAqBM,mBAAA;;;;;;;;UAmEN,eAAA;;;;;;;UAmBA,YAAA;;;;;;;;UAsBA,UAAA;;;;;;;;UAeM,YAAA;;;;;;UAoCN,cAAA;;;;;;UAiBM,gBAAA;;;;;;UAuBA,eAAA;;;;;;UAuBN,iBAAA;AAAA"}
|
|
@@ -227,10 +227,6 @@ var SyncManager = class {
|
|
|
227
227
|
const instruction = this.buildUpdateInstruction(sourceId, config, embedData, options);
|
|
228
228
|
instructions.push(instruction);
|
|
229
229
|
await this.emitSyncingEvent(instruction);
|
|
230
|
-
if (SyncContextManager.canSyncDeeper(options.currentDepth, config.maxSyncDepth)) {
|
|
231
|
-
const nextLevelInstructions = await this.collectNextLevelInstructions(instruction, embedData, changedFields, config, options);
|
|
232
|
-
instructions.push(...nextLevelInstructions);
|
|
233
|
-
}
|
|
234
230
|
}
|
|
235
231
|
return instructions;
|
|
236
232
|
}
|
|
@@ -254,35 +250,6 @@ var SyncManager = class {
|
|
|
254
250
|
return instructions;
|
|
255
251
|
}
|
|
256
252
|
/**
|
|
257
|
-
* Collects instructions for the next level in the sync chain.
|
|
258
|
-
*
|
|
259
|
-
* @param parentInstruction - The parent instruction
|
|
260
|
-
* @param embedData - Embedded data from parent
|
|
261
|
-
* @param changedFields - Changed fields
|
|
262
|
-
* @param parentConfig - Parent sync config
|
|
263
|
-
* @param parentOptions - Parent instruction options
|
|
264
|
-
* @returns Array of next-level sync instructions
|
|
265
|
-
*/
|
|
266
|
-
async collectNextLevelInstructions(parentInstruction, embedData, changedFields, parentConfig, parentOptions) {
|
|
267
|
-
const targetModelClass = parentConfig.targetModelClass;
|
|
268
|
-
const targetSyncConfigs = this.getSyncConfigsForModel(targetModelClass);
|
|
269
|
-
if (targetSyncConfigs.length === 0) return [];
|
|
270
|
-
const nextOptions = {
|
|
271
|
-
currentDepth: parentOptions.currentDepth + 1,
|
|
272
|
-
syncChain: SyncContextManager.extendChain(parentOptions.syncChain, targetModelClass.name),
|
|
273
|
-
maxDepth: Math.min(parentConfig.maxSyncDepth, parentOptions.maxDepth),
|
|
274
|
-
preventCircular: parentOptions.preventCircular && parentConfig.preventCircularSync
|
|
275
|
-
};
|
|
276
|
-
const sourceId = embedData[parentConfig.identifierField];
|
|
277
|
-
return await this.collectInstructions({
|
|
278
|
-
sourceId,
|
|
279
|
-
updatedData: embedData,
|
|
280
|
-
changedFields,
|
|
281
|
-
syncConfigs: targetSyncConfigs,
|
|
282
|
-
options: nextOptions
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
253
|
* Builds an update sync instruction.
|
|
287
254
|
*
|
|
288
255
|
* @param sourceId - Source model ID
|
|
@@ -324,7 +291,7 @@ var SyncManager = class {
|
|
|
324
291
|
buildDeleteInstruction(sourceId, config, options) {
|
|
325
292
|
const targetModelClass = config.targetModelClass;
|
|
326
293
|
const filter = this.buildFilter(sourceId, config);
|
|
327
|
-
const update = { $unset: { [config.targetField]: 1 } };
|
|
294
|
+
const update = config.isMany ? { $pull: { [config.targetField]: { [config.identifierField]: sourceId } } } : { $unset: { [config.targetField]: 1 } };
|
|
328
295
|
return {
|
|
329
296
|
targetTable: targetModelClass.table,
|
|
330
297
|
targetModel: targetModelClass.name,
|
|
@@ -359,8 +326,9 @@ var SyncManager = class {
|
|
|
359
326
|
else return { $set: { [config.targetField]: embedData } };
|
|
360
327
|
}
|
|
361
328
|
/**
|
|
362
|
-
* Executes sync instructions
|
|
363
|
-
*
|
|
329
|
+
* Executes sync instructions, once each, through the driver's dialect-aware
|
|
330
|
+
* sync adapter. Instructions run in depth order and each one records its own
|
|
331
|
+
* outcome, so one failing instruction never re-runs the ones that succeeded.
|
|
364
332
|
*
|
|
365
333
|
* @param instructions - Array of sync instructions
|
|
366
334
|
* @returns Sync result
|
|
@@ -376,73 +344,40 @@ var SyncManager = class {
|
|
|
376
344
|
contexts: []
|
|
377
345
|
};
|
|
378
346
|
if (instructions.length === 0) return result;
|
|
379
|
-
const
|
|
380
|
-
for (const [depth, depthInstructions] of
|
|
347
|
+
const adapter = this.driver.syncAdapter();
|
|
348
|
+
for (const [depth, depthInstructions] of this.groupByDepth(instructions)) {
|
|
381
349
|
result.depthReached = Math.max(result.depthReached, depth);
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
await this.
|
|
350
|
+
for (const instruction of depthInstructions) try {
|
|
351
|
+
const affected = await adapter.executeBatch([instruction]);
|
|
352
|
+
const context = SyncContextManager.createContext(instruction, affected);
|
|
353
|
+
result.contexts.push(context);
|
|
354
|
+
result.succeeded++;
|
|
355
|
+
await this.emitSyncedEvent(context);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
result.failed++;
|
|
358
|
+
const errorMessage = this.formatSyncError(instruction, error);
|
|
359
|
+
const syncError = new Error(errorMessage);
|
|
360
|
+
if (error instanceof Error && error.stack) syncError.stack = error.stack;
|
|
361
|
+
result.errors.push({
|
|
362
|
+
instruction,
|
|
363
|
+
error: syncError
|
|
364
|
+
});
|
|
365
|
+
console.error(`Sync operation failed:`, {
|
|
366
|
+
sourceModel: instruction.sourceModel,
|
|
367
|
+
sourceId: instruction.sourceId,
|
|
368
|
+
targetModel: instruction.targetModel,
|
|
369
|
+
targetTable: instruction.targetTable,
|
|
370
|
+
depth: instruction.depth,
|
|
371
|
+
chain: SyncContextManager.formatChain(instruction.chain),
|
|
372
|
+
filter: instruction.filter,
|
|
373
|
+
error: errorMessage
|
|
374
|
+
});
|
|
388
375
|
}
|
|
389
376
|
}
|
|
390
377
|
result.success = result.failed === 0;
|
|
391
378
|
return result;
|
|
392
379
|
}
|
|
393
380
|
/**
|
|
394
|
-
* Executes instructions in batch.
|
|
395
|
-
*
|
|
396
|
-
* @param instructions - Instructions to execute
|
|
397
|
-
* @param result - Result object to update
|
|
398
|
-
*/
|
|
399
|
-
async executeBatch(instructions, result) {
|
|
400
|
-
for (const instruction of instructions) try {
|
|
401
|
-
const updateResult = await this.driver.updateMany(instruction.targetTable, instruction.filter, instruction.update);
|
|
402
|
-
const context = SyncContextManager.createContext(instruction, updateResult.modifiedCount);
|
|
403
|
-
result.contexts.push(context);
|
|
404
|
-
result.succeeded++;
|
|
405
|
-
await this.emitSyncedEvent(context);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
throw error;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Executes instructions individually (fallback).
|
|
412
|
-
* Provides detailed error reporting for each failed instruction.
|
|
413
|
-
*
|
|
414
|
-
* @param instructions - Instructions to execute
|
|
415
|
-
* @param result - Result object to update
|
|
416
|
-
*/
|
|
417
|
-
async executeIndividual(instructions, result) {
|
|
418
|
-
for (const instruction of instructions) try {
|
|
419
|
-
const updateResult = await this.driver.updateMany(instruction.targetTable, instruction.filter, instruction.update);
|
|
420
|
-
const context = SyncContextManager.createContext(instruction, updateResult.modifiedCount);
|
|
421
|
-
result.contexts.push(context);
|
|
422
|
-
result.succeeded++;
|
|
423
|
-
await this.emitSyncedEvent(context);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
result.failed++;
|
|
426
|
-
const errorMessage = this.formatSyncError(instruction, error);
|
|
427
|
-
const syncError = new Error(errorMessage);
|
|
428
|
-
if (error instanceof Error && error.stack) syncError.stack = error.stack;
|
|
429
|
-
result.errors.push({
|
|
430
|
-
instruction,
|
|
431
|
-
error: syncError
|
|
432
|
-
});
|
|
433
|
-
console.error(`Sync operation failed:`, {
|
|
434
|
-
sourceModel: instruction.sourceModel,
|
|
435
|
-
sourceId: instruction.sourceId,
|
|
436
|
-
targetModel: instruction.targetModel,
|
|
437
|
-
targetTable: instruction.targetTable,
|
|
438
|
-
depth: instruction.depth,
|
|
439
|
-
chain: SyncContextManager.formatChain(instruction.chain),
|
|
440
|
-
filter: instruction.filter,
|
|
441
|
-
error: errorMessage
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
381
|
* Formats a sync error with detailed context.
|
|
447
382
|
*
|
|
448
383
|
* @param instruction - The failed instruction
|
|
@@ -476,21 +411,6 @@ var SyncManager = class {
|
|
|
476
411
|
return new Map([...grouped.entries()].sort((a, b) => a[0] - b[0]));
|
|
477
412
|
}
|
|
478
413
|
/**
|
|
479
|
-
* Groups instructions by target table for batch optimization.
|
|
480
|
-
*
|
|
481
|
-
* @param instructions - Instructions to group
|
|
482
|
-
* @returns Map of table name to instructions
|
|
483
|
-
*/
|
|
484
|
-
groupByTable(instructions) {
|
|
485
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
486
|
-
for (const instruction of instructions) {
|
|
487
|
-
const table = instruction.targetTable;
|
|
488
|
-
if (!grouped.has(table)) grouped.set(table, []);
|
|
489
|
-
grouped.get(table).push(instruction);
|
|
490
|
-
}
|
|
491
|
-
return grouped;
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
414
|
* Checks if sync should proceed based on watched fields.
|
|
495
415
|
*
|
|
496
416
|
* @param config - Sync configuration
|
|
@@ -510,10 +430,12 @@ var SyncManager = class {
|
|
|
510
430
|
*/
|
|
511
431
|
async getEmbedData(data, config) {
|
|
512
432
|
if (data instanceof Model) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
return data.
|
|
433
|
+
const hidden = data.self().hidden ?? [];
|
|
434
|
+
const withoutHidden = (embedded) => Object.fromEntries(Object.entries(embedded).filter(([key]) => !hidden.includes(key)));
|
|
435
|
+
if (Array.isArray(config.embedKey)) return withoutHidden(data.only(config.embedKey));
|
|
436
|
+
if (typeof data[config.embedKey] !== "function") return withoutHidden(data[config.embedKey]);
|
|
437
|
+
if (data.self().embed) return withoutHidden(data.embedData);
|
|
438
|
+
return data.toJSON();
|
|
517
439
|
}
|
|
518
440
|
return data;
|
|
519
441
|
}
|
|
@@ -528,17 +450,6 @@ var SyncManager = class {
|
|
|
528
450
|
return syncWith.map((builder) => typeof builder.build === "function" ? builder.build() : builder);
|
|
529
451
|
}
|
|
530
452
|
/**
|
|
531
|
-
* Gets sync configurations for a specific model.
|
|
532
|
-
*
|
|
533
|
-
* @param modelClass - The model class
|
|
534
|
-
* @returns Array of sync configurations
|
|
535
|
-
*/
|
|
536
|
-
getSyncConfigsForModel(modelClass) {
|
|
537
|
-
const syncWith = modelClass.syncWith;
|
|
538
|
-
if (!syncWith || !Array.isArray(syncWith)) return [];
|
|
539
|
-
return syncWith.map((builder) => typeof builder.build === "function" ? builder.build() : builder);
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
453
|
* Emits a syncing event.
|
|
543
454
|
*
|
|
544
455
|
* @param instruction - The sync instruction
|