@warlock.js/cascade 5.20.0 → 5.22.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 +40 -1
- package/cjs/index.cjs +2460 -1049
- package/cjs/index.cjs.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 +47 -16
- 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 +193 -57
- 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 +13 -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-ttl-purge.mjs +98 -0
- package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -0
- 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 +5 -6
- package/esm/index.mjs +4 -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 +369 -146
- 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 +22 -8
- package/esm/remover/database-remover.mjs.map +1 -1
- package/esm/restorer/database-restorer.d.mts +11 -2
- package/esm/restorer/database-restorer.d.mts.map +1 -1
- package/esm/restorer/database-restorer.mjs +45 -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-fanout.mjs +67 -0
- package/esm/sync/sync-fanout.mjs.map +1 -0
- 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/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 +85 -18
- package/esm/writer/database-writer.mjs.map +1 -1
- package/package.json +8 -7
- 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":"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"}
|
|
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 { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { DatabaseWriter } from \"../writer/database-writer\";\n\n/**\n * Database restorer service that orchestrates model restoration.\n *\n * Handles the complete restoration pipeline:\n * 1. Strategy detection (trash vs soft delete)\n * 2. Record retrieval from trash table or soft-deleted records\n * 3. ID conflict resolution\n * 4. Event emission (restoring, restored)\n * 5. Driver execution (insert back to original table, remove from trash/clear deletedAt)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * const result = await restorer.restore(123);\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"soft\"\n * ```\n */\nexport class DatabaseRestorer implements RestorerContract {\n /** Model constructor reference */\n private readonly ctor: typeof Model;\n\n /** Data source containing driver */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /**\n * Create a new restorer instance for a model class.\n *\n * @param modelClass - The model class (static context)\n *\n * @example\n * ```typescript\n * const restorer = new DatabaseRestorer(User);\n * await restorer.restore(123);\n * ```\n */\n public constructor(modelClass: typeof Model) {\n this.ctor = modelClass;\n this.dataSource = modelClass.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = modelClass.table;\n this.primaryKey = modelClass.primaryKey;\n }\n\n /**\n * Restore a single deleted record by its ID.\n *\n * @param id - The primary key value of the record to restore\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and restored record\n * @throws {Error} If record not found in trash or soft-deleted records\n * @throws {Error} If ID conflict and onIdConflict is \"fail\"\n */\n public async restore(\n id: string | number,\n options: RestorerOptions = {},\n ): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch record based on strategy\n const recordData = await this.fetchRecordByStrategy(id, strategy);\n\n if (!recordData) {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: record not found in ${strategy === \"trash\" ? \"trash table\" : \"soft-deleted records\"}.`,\n );\n }\n\n // 4. Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // 5. Check for ID conflict and handle. A soft-deleted row never left the\n // table, so it would \"conflict\" with itself: it keeps its id and is\n // updated in place.\n const finalData =\n strategy === \"trash\"\n ? await this.handleIdConflict(restoredData, id, onIdConflict)\n : restoredData;\n\n // 6. Create temporary model instance for event emission\n // Note: Model is abstract, but at runtime this.ctor is a concrete subclass\n const model = new (this.ctor as any)(finalData) as Model;\n\n // 7. Emit restoring event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // 8. Restore based on strategy\n if (strategy === \"trash\") {\n // Insert record back into original table (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n\n // Remove from trash table\n await this.driver.delete(this.resolveTrashTable(), {\n [this.primaryKey]: id,\n originalTable: this.table,\n });\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt (don't insert - would create duplicate!)\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n // 10. Emit restored event (unless skipEvents)\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n\n return {\n success: true,\n restoredCount: 1,\n strategy,\n restoredRecord: model,\n };\n }\n\n /**\n * Restore all deleted records for the model's table.\n *\n * @param options - Restorer options\n * @returns Result containing success status, strategy used, and aggregate counts\n */\n public async restoreAll(options: RestorerOptions = {}): Promise<RestorerResult> {\n const onIdConflict = options.onIdConflict ?? \"assignNew\";\n const skipEvents = options.skipEvents ?? false;\n\n // 1. Resolve strategy (options → model static → data source default)\n const strategy = this.resolveStrategy(options.strategy);\n\n // 2. Validate strategy (cannot restore permanent deletes)\n if (strategy === \"permanent\") {\n throw new Error(\n `Cannot restore all ${this.ctor.name} records: permanently deleted records cannot be restored.`,\n );\n }\n\n // 3. Fetch all records based on strategy\n const recordsToRestore = await this.fetchAllRecordsByStrategy(strategy);\n\n if (recordsToRestore.length === 0) {\n return {\n success: true,\n restoredCount: 0,\n strategy,\n };\n }\n\n // 4. Restore each record\n let restoredCount = 0;\n const conflicts: Array<{ id: string | number; reason: string }> = [];\n\n const restoredRecords: Model[] = [];\n\n for (const recordData of recordsToRestore) {\n const id = recordData[this.primaryKey] as string | number;\n\n try {\n // Prepare record data (remove metadata fields)\n const restoredData = { ...recordData };\n delete restoredData.deletedAt;\n delete restoredData.originalTable;\n\n // Check for ID conflict\n // Soft rows never left the table; only trash restores can conflict.\n const idExists = strategy === \"trash\" && (await this.checkIdExists(id));\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${id}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n const finalData = await this.assignNewId(restoredData);\n conflicts.push({\n id,\n reason: `ID ${id} already exists, assigned new ID ${finalData[this.primaryKey]}`,\n });\n\n // Create temporary model for events\n const model = new (this.ctor as any)(finalData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with new ID (was moved to trash)\n await this.driver.insert(this.table, finalData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n } else {\n // No conflict, restore with original ID\n const model = new (this.ctor as any)(restoredData) as Model;\n\n // Emit restoring event\n if (!skipEvents) {\n await model.emitEvent(\"restoring\");\n }\n\n // Restore based on strategy\n if (strategy === \"trash\") {\n // Insert with original ID (was moved to trash)\n await this.driver.insert(this.table, restoredData);\n model.isNew = false;\n } else if (strategy === \"soft\") {\n // Record still exists, just unset deletedAt\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (deletedAtColumn) {\n const filter = { [this.primaryKey]: id };\n const updateOperations: UpdateOperations = {\n $unset: { [deletedAtColumn]: 1 },\n };\n await this.driver.update(this.table, filter, updateOperations);\n model.isNew = false;\n }\n }\n\n restoredRecords.push(model);\n\n // Emit restored event\n if (!skipEvents) {\n await model.emitEvent(\"restored\");\n }\n\n await this.triggerSync(model);\n }\n\n // Remove from trash (only for trash strategy)\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n const trashFilter = { [this.primaryKey]: id, originalTable: this.table };\n await this.driver.delete(trashTable, trashFilter);\n }\n\n restoredCount++;\n } catch (error) {\n if (onIdConflict === \"fail\") {\n throw error;\n }\n // Continue with next record if assignNew mode\n conflicts.push({\n id,\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n return {\n success: true,\n restoredCount,\n restoredRecords,\n strategy,\n conflicts: conflicts.length > 0 ? conflicts : undefined,\n };\n }\n\n /**\n * Resolve the delete strategy.\n *\n * Priority: options → model static → data source default → \"permanent\"\n *\n * @param strategyOption - Optional strategy override from options\n * @returns The resolved delete strategy\n * @private\n */\n private resolveStrategy(strategyOption?: \"trash\" | \"soft\"): DeleteStrategy {\n return (\n strategyOption ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\"\n );\n }\n\n /**\n * Fetch a record by ID based on the delete strategy.\n *\n * @param id - The primary key value\n * @param strategy - The delete strategy to use\n * @returns The record data, or null if not found\n * @private\n */\n private async fetchRecordByStrategy(id: string | number, strategy: \"trash\" | \"soft\") {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = await this.driver\n .queryBuilder(trashTable)\n .where(this.primaryKey, id)\n .where(\"originalTable\", this.table)\n .first<Record<string, unknown>>();\n\n return trashQuery;\n } catch {\n return null;\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return null;\n }\n\n try {\n const softDeletedQuery = await this.driver\n .queryBuilder(this.table)\n .where(this.primaryKey, id)\n .whereNotNull(deletedAtColumn)\n .first<Record<string, unknown>>();\n\n return softDeletedQuery;\n } catch {\n return null;\n }\n }\n\n return null;\n }\n\n /**\n * Fetch all records based on the delete strategy.\n *\n * @param strategy - The delete strategy to use\n * @returns Array of record data\n * @private\n */\n private async fetchAllRecordsByStrategy(\n strategy: \"trash\" | \"soft\",\n ): Promise<Record<string, unknown>[]> {\n if (strategy === \"trash\") {\n const trashTable = this.resolveTrashTable();\n try {\n const trashQuery = this.driver\n .queryBuilder(trashTable)\n .where(\"originalTable\", this.table)\n .get();\n\n return (await trashQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n } else if (strategy === \"soft\") {\n const deletedAtColumn = this.ctor.deletedAtColumn ?? \"deletedAt\";\n if (!deletedAtColumn) {\n return [];\n }\n\n try {\n const softDeletedQuery = this.driver\n .queryBuilder(this.table)\n .whereNotNull(deletedAtColumn)\n .get();\n\n return (await softDeletedQuery) as Record<string, unknown>[];\n } catch {\n return [];\n }\n }\n\n return [];\n }\n\n /**\n * Handle ID conflict by checking if ID exists and assigning new one if needed.\n *\n * @param recordData - The record data to restore\n * @param originalId - The original ID value\n * @param onIdConflict - Conflict resolution strategy\n * @returns Record data with potentially new ID\n * @private\n */\n private async handleIdConflict(\n recordData: Record<string, unknown>,\n originalId: string | number,\n onIdConflict: \"fail\" | \"assignNew\",\n ): Promise<Record<string, unknown>> {\n const idExists = await this.checkIdExists(originalId);\n\n if (idExists) {\n if (onIdConflict === \"fail\") {\n throw new Error(\n `Cannot restore ${this.ctor.name} with ${this.primaryKey} ${originalId}: ID already exists in target table.`,\n );\n }\n\n // Assign new ID\n return await this.assignNewId(recordData);\n }\n\n return recordData;\n }\n\n /**\n * Check if an ID already exists in the target table.\n *\n * @param id - The ID to check\n * @returns True if ID exists, false otherwise\n * @private\n */\n private async checkIdExists(id: string | number): Promise<boolean> {\n try {\n const query = this.driver.queryBuilder(this.table).where(this.primaryKey, id).exists();\n\n return await query;\n } catch {\n return false;\n }\n }\n\n /**\n * Assign a new ID to the record data.\n *\n * For MongoDB: drops the old `_id` and `id`, then runs the record through the\n * writer's insert preparation so the new `id` comes from the same generator\n * a normal save uses (validation and events are skipped; this is a restore)\n * For SQL: Removes the primary key to let database auto-increment\n *\n * @param recordData - The record data\n * @returns Record data with new ID assigned\n * @private\n */\n private async assignNewId(recordData: Record<string, unknown>): Promise<Record<string, unknown>> {\n const newData = { ...recordData };\n\n if (this.driver.name !== \"mongodb\") {\n // SQL: Remove primary key to let database auto-increment\n delete newData[this.primaryKey];\n\n return newData;\n }\n\n // MongoDB: let the driver mint a new _id and the writer generate a new id.\n // The generator skips records that already carry an `id`, so drop both.\n delete newData._id;\n delete newData.id;\n\n const model = new (this.ctor as any)(newData) as Model;\n\n return await new DatabaseWriter(model).prepareForInsert({\n skipEvents: true,\n skipValidation: true,\n });\n }\n\n /**\n * Resolve the trash table/collection name.\n *\n * Priority:\n * 1. Model.trashTable (if set)\n * 2. Data source defaultTrashTable (e.g., \"RecycleBin\" for MongoDB)\n * 3. Default pattern: `{table}Trash`\n *\n * @returns The trash table/collection name\n * @private\n */\n private resolveTrashTable(): string {\n if (this.ctor.trashTable) {\n return this.ctor.trashTable;\n }\n\n if (this.dataSource.defaultTrashTable) {\n return this.dataSource.defaultTrashTable;\n }\n\n return `${this.table}Trash`;\n }\n\n /**\n * Queue sync for a restored record so embedded copies come back up to date.\n * The restore has already succeeded, so queued failures are retried separately.\n *\n * @private\n */\n private triggerSync(model: Model): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"restore\",\n execute: () =>\n triggerModelEvent(\n this.ctor as any,\n \"updated\",\n model,\n Object.keys(model.data as Record<string, unknown>),\n ),\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,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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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { log } from "@warlock.js/logger";
|
|
2
|
+
|
|
3
|
+
//#region ../cascade/src/sync/sync-fanout.ts
|
|
4
|
+
/**
|
|
5
|
+
* Serializes post-commit sync fan-out and retries transient failures without
|
|
6
|
+
* making the already-committed write, delete, or restore fail.
|
|
7
|
+
*/
|
|
8
|
+
var SyncFanoutQueue = class {
|
|
9
|
+
jobs = [];
|
|
10
|
+
maxAttempts;
|
|
11
|
+
retryDelay;
|
|
12
|
+
reportFailure;
|
|
13
|
+
draining = false;
|
|
14
|
+
idleResolvers = [];
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.maxAttempts = options.maxAttempts ?? 3;
|
|
17
|
+
this.retryDelay = options.retryDelay ?? ((attempt) => attempt * 50);
|
|
18
|
+
this.reportFailure = options.reportFailure ?? ((job, error) => {
|
|
19
|
+
log.error("database", "sync.failed", `[cascade] ${job.operation} sync failed for ${job.sourceModel} after ${this.maxAttempts} attempts: ${error}`);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Add a committed lifecycle fan-out job without awaiting its execution. */
|
|
23
|
+
enqueue(job) {
|
|
24
|
+
this.jobs.push(job);
|
|
25
|
+
if (!this.draining) {
|
|
26
|
+
this.draining = true;
|
|
27
|
+
this.drain();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Wait until the currently queued fan-out jobs have settled. */
|
|
31
|
+
async flush() {
|
|
32
|
+
if (!this.draining && this.jobs.length === 0) return;
|
|
33
|
+
await new Promise((resolve) => this.idleResolvers.push(resolve));
|
|
34
|
+
}
|
|
35
|
+
async drain() {
|
|
36
|
+
while (this.jobs.length > 0) {
|
|
37
|
+
const job = this.jobs.shift();
|
|
38
|
+
if (job) await this.execute(job);
|
|
39
|
+
}
|
|
40
|
+
this.draining = false;
|
|
41
|
+
this.idleResolvers.splice(0).forEach((resolve) => resolve());
|
|
42
|
+
}
|
|
43
|
+
async execute(job) {
|
|
44
|
+
let error;
|
|
45
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) try {
|
|
46
|
+
await job.execute();
|
|
47
|
+
return;
|
|
48
|
+
} catch (caught) {
|
|
49
|
+
error = caught;
|
|
50
|
+
if (attempt < this.maxAttempts) await this.wait(this.retryDelay(attempt));
|
|
51
|
+
}
|
|
52
|
+
this.reportFailure(job, error);
|
|
53
|
+
}
|
|
54
|
+
async wait(milliseconds) {
|
|
55
|
+
if (milliseconds <= 0) return;
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const syncFanoutQueue = new SyncFanoutQueue();
|
|
60
|
+
/** Queue a lifecycle sync after its database change has committed. */
|
|
61
|
+
function enqueueSyncFanout(job) {
|
|
62
|
+
syncFanoutQueue.enqueue(job);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { enqueueSyncFanout };
|
|
67
|
+
//# sourceMappingURL=sync-fanout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-fanout.mjs","names":[],"sources":["../../../../../../../cascade/src/sync/sync-fanout.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\n\ntype SyncFanoutJob = {\n sourceModel: string;\n operation: string;\n execute: () => Promise<void>;\n};\n\ntype SyncFanoutQueueOptions = {\n maxAttempts?: number;\n retryDelay?: (attempt: number) => number;\n reportFailure?: (job: SyncFanoutJob, error: unknown) => void;\n};\n\n/**\n * Serializes post-commit sync fan-out and retries transient failures without\n * making the already-committed write, delete, or restore fail.\n */\nexport class SyncFanoutQueue {\n private readonly jobs: SyncFanoutJob[] = [];\n private readonly maxAttempts: number;\n private readonly retryDelay: (attempt: number) => number;\n private readonly reportFailure: (job: SyncFanoutJob, error: unknown) => void;\n private draining = false;\n private idleResolvers: Array<() => void> = [];\n\n public constructor(options: SyncFanoutQueueOptions = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.retryDelay = options.retryDelay ?? ((attempt) => attempt * 50);\n this.reportFailure = options.reportFailure ?? ((job, error) => {\n log.error(\n \"database\",\n \"sync.failed\",\n `[cascade] ${job.operation} sync failed for ${job.sourceModel} after ${this.maxAttempts} attempts: ${error}`,\n );\n });\n }\n\n /** Add a committed lifecycle fan-out job without awaiting its execution. */\n public enqueue(job: SyncFanoutJob): void {\n this.jobs.push(job);\n\n if (!this.draining) {\n this.draining = true;\n void this.drain();\n }\n }\n\n /** Wait until the currently queued fan-out jobs have settled. */\n public async flush(): Promise<void> {\n if (!this.draining && this.jobs.length === 0) {\n return;\n }\n\n await new Promise<void>((resolve) => this.idleResolvers.push(resolve));\n }\n\n private async drain(): Promise<void> {\n while (this.jobs.length > 0) {\n const job = this.jobs.shift();\n\n if (job) {\n await this.execute(job);\n }\n }\n\n this.draining = false;\n this.idleResolvers.splice(0).forEach((resolve) => resolve());\n }\n\n private async execute(job: SyncFanoutJob): Promise<void> {\n let error: unknown;\n\n for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n try {\n await job.execute();\n return;\n } catch (caught) {\n error = caught;\n\n if (attempt < this.maxAttempts) {\n await this.wait(this.retryDelay(attempt));\n }\n }\n }\n\n this.reportFailure(job, error);\n }\n\n private async wait(milliseconds: number): Promise<void> {\n if (milliseconds <= 0) {\n return;\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));\n }\n}\n\nconst syncFanoutQueue = new SyncFanoutQueue();\n\n/** Queue a lifecycle sync after its database change has committed. */\nexport function enqueueSyncFanout(job: SyncFanoutJob): void {\n syncFanoutQueue.enqueue(job);\n}\n"],"mappings":";;;;;;;AAkBA,IAAa,kBAAb,MAA6B;CAC3B,AAAiB,OAAwB,CAAC;CAC1C,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAQ,WAAW;CACnB,AAAQ,gBAAmC,CAAC;CAE5C,AAAO,YAAY,UAAkC,CAAC,GAAG;EACvD,KAAK,cAAc,QAAQ,eAAe;EAC1C,KAAK,aAAa,QAAQ,gBAAgB,YAAY,UAAU;EAChE,KAAK,gBAAgB,QAAQ,mBAAmB,KAAK,UAAU;GAC7D,IAAI,MACF,YACA,eACA,aAAa,IAAI,UAAU,mBAAmB,IAAI,YAAY,SAAS,KAAK,YAAY,aAAa,OACvG;EACF;CACF;;CAGA,AAAO,QAAQ,KAA0B;EACvC,KAAK,KAAK,KAAK,GAAG;EAElB,IAAI,CAAC,KAAK,UAAU;GAClB,KAAK,WAAW;GAChB,AAAK,KAAK,MAAM;EAClB;CACF;;CAGA,MAAa,QAAuB;EAClC,IAAI,CAAC,KAAK,YAAY,KAAK,KAAK,WAAW,GACzC;EAGF,MAAM,IAAI,SAAe,YAAY,KAAK,cAAc,KAAK,OAAO,CAAC;CACvE;CAEA,MAAc,QAAuB;EACnC,OAAO,KAAK,KAAK,SAAS,GAAG;GAC3B,MAAM,MAAM,KAAK,KAAK,MAAM;GAE5B,IAAI,KACF,MAAM,KAAK,QAAQ,GAAG;EAE1B;EAEA,KAAK,WAAW;EAChB,KAAK,cAAc,OAAO,CAAC,CAAC,CAAC,SAAS,YAAY,QAAQ,CAAC;CAC7D;CAEA,MAAc,QAAQ,KAAmC;EACvD,IAAI;EAEJ,KAAK,IAAI,UAAU,GAAG,WAAW,KAAK,aAAa,WACjD,IAAI;GACF,MAAM,IAAI,QAAQ;GAClB;EACF,SAAS,QAAQ;GACf,QAAQ;GAER,IAAI,UAAU,KAAK,aACjB,MAAM,KAAK,KAAK,KAAK,WAAW,OAAO,CAAC;EAE5C;EAGF,KAAK,cAAc,KAAK,KAAK;CAC/B;CAEA,MAAc,KAAK,cAAqC;EACtD,IAAI,gBAAgB,GAClB;EAGF,MAAM,IAAI,SAAe,YAAY,WAAW,SAAS,YAAY,CAAC;CACxE;AACF;AAEA,MAAM,kBAAkB,IAAI,gBAAgB;;AAG5C,SAAgB,kBAAkB,KAA0B;CAC1D,gBAAgB,QAAQ,GAAG;AAC7B"}
|
|
@@ -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"}
|