@warlock.js/cascade 5.30.0 → 5.31.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 +6 -0
- package/README.md +1 -5
- package/cjs/index.cjs +0 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-restorer.contract.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-operations.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/naming.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +0 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -1
- package/esm/drivers/postgres/types.d.mts.map +1 -1
- package/esm/errors/unsupported-query-operation.error.mjs.map +1 -1
- package/esm/expressions/column-expressions.d.mts.map +1 -1
- package/esm/expressions/column-expressions.mjs.map +1 -1
- package/esm/migration/foreign-key-builder.mjs.map +1 -1
- package/esm/migration/parse-created-at.mjs.map +1 -1
- package/esm/migration/sql-grammar.mjs.map +1 -1
- package/esm/model/generate-model-id.mjs.map +1 -1
- package/esm/model/methods/accessor-methods.mjs.map +1 -1
- package/esm/model/methods/dirty-methods.mjs.map +1 -1
- package/esm/model/methods/hydration-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/methods/scope-methods.mjs.map +1 -1
- package/esm/model/methods/static-event-methods.mjs.map +1 -1
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs.map +1 -1
- package/esm/model/register-model.d.mts.map +1 -1
- package/esm/model/register-model.mjs.map +1 -1
- package/esm/operations/migrations.d.mts.map +1 -1
- package/esm/operations/migrations.mjs.map +1 -1
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/relations/key-conventions.mjs.map +1 -1
- package/esm/relations/pivot-operations.d.mts.map +1 -1
- package/esm/relations/pivot-operations.mjs.map +1 -1
- package/esm/relations/relation-loader.d.mts.map +1 -1
- 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.map +1 -1
- package/esm/sync/sync-context.d.mts.map +1 -1
- package/esm/sync/sync-context.mjs.map +1 -1
- package/esm/sync/sync-fanout.mjs.map +1 -1
- package/esm/sync/sync-manager.d.mts.map +1 -1
- package/esm/sync/sync-manager.mjs.map +1 -1
- package/esm/utils/database-writer.utils.d.mts.map +1 -1
- package/esm/utils/database-writer.utils.mjs.map +1 -1
- package/esm/utils/is-valid-date-value.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +541 -434
- package/package.json +5 -5
- package/skills/aggregate-data/SKILL.md +28 -17
- package/skills/alter-migration/SKILL.md +325 -233
- package/skills/cascade-basics/SKILL.md +19 -19
- package/skills/configure-delete-strategy/SKILL.md +11 -11
- package/skills/define-model/SKILL.md +19 -17
- package/skills/define-relations/SKILL.md +13 -18
- package/skills/index.md +3 -0
- package/skills/manage-transactions/SKILL.md +18 -12
- package/skills/paginate-results/SKILL.md +6 -8
- package/skills/perform-atomic-ops/SKILL.md +26 -21
- package/skills/query-data/SKILL.md +27 -33
- package/skills/run-cascade-cli/SKILL.md +11 -10
- package/skills/search-by-vector/SKILL.md +8 -5
- package/skills/subscribe-to-model-events/SKILL.md +17 -17
- package/skills/track-changes/SKILL.md +9 -9
- package/skills/write-migration/SKILL.md +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relation-loader.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/relation-loader.ts"],"sourcesContent":["/**\n * @fileoverview Core relation loading logic for the Cascade ORM.\n *\n * The RelationLoader is responsible for efficiently loading related models\n * while preventing N+1 query problems through batch loading strategies.\n *\n * @module @warlock.js/cascade/relations/relation-loader\n */\n\nimport { isLazy, type Lazy } from \"@mongez/reinforcements\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelFromRegistry, resolveModelName } from \"../model/register-model\";\nimport {\n inferBelongsToForeignKey,\n inferHasForeignKey,\n inferPivotKey,\n inferPivotTable,\n} from \"./key-conventions\";\nimport type {\n LoadedRelationResult,\n RelationConstraintCallback,\n RelationConstraints,\n RelationDefinition,\n} from \"./types\";\n\n// ============================================================================\n// SHARED HELPER — SINGLE SOURCE OF TRUTH FOR LOADED RELATIONS\n// ============================================================================\n\n/**\n * Attach a loaded relation onto a model instance, keeping the\n * `loadedRelations` Map and the direct property access in sync.\n *\n * The property is installed as a `defineProperty` getter/setter façade over\n * the Map — `model.posts` reads from `loadedRelations.get(\"posts\")`, and\n * assigning `model.posts = newPosts` writes back to the Map. Eliminates the\n * historical drift between the two storage slots: any mutation visible via\n * one path is visible via the other.\n *\n * Used by both `RelationLoader` (the `with()` path) and the Postgres\n * driver's `attachJoinedRelations` (the `joinWith()` path).\n *\n * @example\n * attachLoadedRelation(user, \"posts\", postsArray);\n * user.posts === user.loadedRelations.get(\"posts\"); // always true\n */\nexport function attachLoadedRelation(\n model: object,\n name: string,\n value: LoadedRelationResult,\n): void {\n const modelWithRelations = model as {\n loadedRelations?: Map<string, LoadedRelationResult>;\n };\n\n if (!modelWithRelations.loadedRelations) {\n modelWithRelations.loadedRelations = new Map();\n }\n\n const relations = modelWithRelations.loadedRelations;\n relations.set(name, value);\n\n // Install a getter/setter on the instance so direct property access reads\n // from (and writes to) the Map. `configurable: true` lets a later reload\n // re-define the property; `enumerable: true` so JSON.stringify and\n // Object.keys see the relation.\n Object.defineProperty(model, name, {\n configurable: true,\n enumerable: true,\n get(): LoadedRelationResult | undefined {\n return relations.get(name);\n },\n set(next: LoadedRelationResult): void {\n relations.set(name, next);\n },\n });\n}\n\n// ============================================================================\n// RELATION LOADER CLASS\n// ============================================================================\n\n/**\n * Efficiently loads relationships for one or more model instances.\n *\n * The RelationLoader handles:\n * - Batch loading to prevent N+1 query problems\n * - Nested relation loading via dot notation\n * - Constrained loading with query callbacks\n * - All relation types (hasOne, hasMany, belongsTo, belongsToMany)\n *\n * @template TModel - The type of model being loaded\n *\n * @example\n * ```typescript\n * const users = await User.query().get();\n * const loader = new RelationLoader(users, User);\n *\n * // Load single relation\n * await loader.load(\"posts\");\n *\n * // Load multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // Load nested relations\n * await loader.load(\"posts.comments.author\");\n *\n * // Load with constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"isPublished\", true),\n * });\n * ```\n */\nexport class RelationLoader<TModel extends Model = Model> {\n // ==========================================================================\n // PRIVATE PROPERTIES\n // ==========================================================================\n\n /**\n * The model instances to load relations for.\n */\n private readonly models: TModel[];\n\n /**\n * The model class constructor.\n */\n private readonly modelClass: ChildModel<TModel>;\n\n // ==========================================================================\n // CONSTRUCTOR\n // ==========================================================================\n\n /**\n * Creates a new RelationLoader instance.\n *\n * @param models - The model instances to load relations for\n * @param modelClass - The model class constructor\n */\n public constructor(models: TModel[], modelClass: ChildModel<TModel>) {\n this.models = models;\n this.modelClass = modelClass;\n }\n\n /**\n * Read the configured relation conventions from this model's data\n * source. Returns `undefined` when no overrides are set — the inference\n * helpers fall back to framework defaults in that case.\n */\n private get relationDefaults() {\n try {\n const dataSource = this.modelClass.getDataSource();\n\n if (!dataSource) return undefined;\n\n const namingConvention =\n dataSource.modelDefaults?.namingConvention ??\n dataSource.driver?.modelDefaults?.namingConvention;\n\n return { ...dataSource.relationDefaults, namingConvention };\n } catch {\n return undefined;\n }\n }\n\n // ==========================================================================\n // PUBLIC METHODS\n // ==========================================================================\n\n /**\n * Loads one or more relations for all model instances.\n *\n * @param relations - A single relation name, array of names, or constraints object\n * @param constraints - Optional constraints to apply to the loading query\n *\n * @example\n * ```typescript\n * // Single relation\n * await loader.load(\"posts\");\n *\n * // Multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // With constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"status\", \"active\"),\n * });\n * ```\n */\n public async load(\n relations: string | string[],\n constraints?: RelationConstraints,\n ): Promise<void> {\n // If no models, nothing to load\n if (this.models.length === 0) {\n return;\n }\n\n // Normalize to array\n const relationNames = Array.isArray(relations) ? relations : [relations];\n\n // Group the paths into a tree keyed by root relation, so a shared prefix\n // (\"posts.comments\", \"posts.tags\") is loaded ONCE and every child path is\n // loaded onto those same instances instead of overwriting them.\n const nodes = new Map<\n string,\n {\n constraint?: RelationConstraintCallback;\n children: string[];\n childConstraints: RelationConstraints;\n }\n >();\n\n for (const relationName of relationNames) {\n const [root, ...rest] = this.parseNestedRelation(relationName);\n const key = root ?? relationName;\n\n if (!nodes.has(key)) {\n nodes.set(key, { children: [], childConstraints: {} });\n }\n\n const node = nodes.get(key)!;\n const constraint = constraints?.[relationName];\n const callbackConstraint = typeof constraint === \"function\" ? constraint : undefined;\n\n if (rest.length === 0) {\n // A constraint belongs to the LAST segment of its path only.\n if (callbackConstraint) node.constraint = callbackConstraint;\n continue;\n }\n\n const childPath = rest.join(\".\");\n\n if (!node.children.includes(childPath)) {\n node.children.push(childPath);\n }\n\n if (callbackConstraint) node.childConstraints[childPath] = callbackConstraint;\n }\n\n for (const [root, node] of nodes) {\n await this.loadRelation(root, node.constraint, node.children, node.childConstraints);\n }\n }\n\n // ==========================================================================\n // PRIVATE METHODS - RELATION LOADING\n // ==========================================================================\n\n /**\n * Loads a single (root) relation once, then its child paths onto the same\n * loaded instances.\n *\n * @param name - The root relation name\n * @param constraint - Optional constraint callback, applied to this relation only\n * @param children - Remaining dot paths below this relation\n * @param childConstraints - Constraints keyed by the remaining dot paths\n */\n private async loadRelation(\n name: string,\n constraint?: RelationConstraintCallback,\n children: string[] = [],\n childConstraints: RelationConstraints = {},\n ): Promise<void> {\n const rootRelation: string | undefined = name || undefined;\n\n // An empty parse would otherwise reach `getRelationDefinition(undefined)`\n // and fail as `Relation \"undefined\" is not defined on model \"X\"` — a\n // message that sends the reader looking for a relation they never wrote,\n // instead of at the name they passed. Same failure, named correctly.\n if (rootRelation === undefined) {\n throw new Error(\n `Relation name \"${name}\" could not be parsed into a relation path on model ` +\n `\"${this.modelClass.name}\".`,\n );\n }\n\n // Get the relation definition from the model class\n const definition = this.getRelationDefinition(rootRelation);\n\n if (!definition) {\n throw new Error(\n `Relation \"${rootRelation}\" is not defined on model \"${this.modelClass.name}\". ` +\n `Make sure to define it in the static 'relations' property.`,\n );\n }\n\n // Load based on relation type\n switch (definition.type) {\n case \"hasMany\":\n await this.loadHasMany(rootRelation, definition, constraint);\n break;\n\n case \"hasOne\":\n await this.loadHasOne(rootRelation, definition, constraint);\n break;\n\n case \"belongsTo\":\n await this.loadBelongsTo(rootRelation, definition, constraint);\n break;\n\n case \"belongsToMany\":\n await this.loadBelongsToMany(rootRelation, definition, constraint);\n break;\n }\n\n // If there are nested relations, load them recursively\n if (children.length > 0) {\n await this.loadNestedRelations(rootRelation, children, childConstraints);\n }\n }\n\n /**\n * Loads a hasMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey = definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n // No values to query, set empty arrays\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Group by foreign key and assign to models\n const recordsByForeignKey = this.groupBy(relatedRecords as Model[], foreignKey);\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? [];\n });\n }\n\n /**\n * Loads a hasOne relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasOne(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey = definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by foreign key\n const recordsByForeignKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const fkValue = record.get(foreignKey);\n // For hasOne, take the first match only\n if (!recordsByForeignKey.has(fkValue)) {\n recordsByForeignKey.set(fkValue, record);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsTo relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsTo(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const foreignKey = definition.foreignKey ?? inferBelongsToForeignKey(name, this.relationDefaults);\n const ownerKey = definition.localKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all foreign key values from the models\n const foreignKeyValues = this.collectKeyValues(foreignKey);\n\n if (foreignKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(ownerKey, foreignKeyValues);\n\n this.applySelect(query, definition, [ownerKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by owner key\n const recordsByOwnerKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const keyValue = record.get(ownerKey);\n recordsByOwnerKey.set(keyValue, record);\n }\n\n this.setRelationOnModels(name, (model) => {\n const fkValue = model.get(foreignKey);\n return recordsByOwnerKey.get(fkValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsToMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsToMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const relatedModelName = resolveModelName(definition.model);\n const pivotTable =\n definition.pivot ??\n inferPivotTable(this.modelClass.name, relatedModelName, this.relationDefaults);\n const localKey = definition.pivotLocalKey ?? this.modelClass.primaryKey ?? \"id\";\n const pivotLocalKey =\n definition.localKey ?? inferPivotKey(this.modelClass.name, this.relationDefaults);\n const pivotForeignKey =\n definition.foreignKey ?? inferPivotKey(relatedModelName, this.relationDefaults);\n const relatedKey = definition.pivotForeignKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 1: Query the pivot table to get the relationships\n const dataSource = this.modelClass.getDataSource();\n const pivotQuery = dataSource.driver\n .queryBuilder(pivotTable)\n .whereIn(pivotLocalKey, localKeyValues);\n\n const pivotRecords = (await pivotQuery.get()) as Record<string, unknown>[];\n\n if (pivotRecords.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 2: Collect related model IDs from pivot\n const relatedIds = [...new Set(pivotRecords.map((p) => p[pivotForeignKey]))];\n\n // Step 3: Query the related model\n const relatedQuery = RelatedModel.query().whereIn(relatedKey, relatedIds);\n\n this.applySelect(relatedQuery, definition, [relatedKey]);\n\n if (constraint) {\n constraint(relatedQuery);\n }\n\n const relatedRecords = await relatedQuery.get();\n\n // Step 4: Index related records by their key\n const relatedById = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n relatedById.set(record.get(relatedKey), record);\n }\n\n // Step 5: Build the relationship map from pivot data\n const relationshipMap = new Map<unknown, Model[]>();\n\n for (const pivot of pivotRecords) {\n const localValue = pivot[pivotLocalKey];\n const foreignValue = pivot[pivotForeignKey];\n const relatedRecord = relatedById.get(foreignValue);\n\n if (relatedRecord) {\n if (!relationshipMap.has(localValue)) {\n relationshipMap.set(localValue, []);\n }\n relationshipMap.get(localValue)!.push(relatedRecord);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return relationshipMap.get(keyValue) ?? [];\n });\n }\n\n // ==========================================================================\n // PRIVATE METHODS - NESTED RELATIONS\n // ==========================================================================\n\n /**\n * Loads nested relations recursively.\n *\n * @param parentRelation - The name of the parent relation\n * @param remainingPaths - The remaining dot paths to load below the parent\n * @param constraints - Constraints keyed by those remaining paths\n */\n private async loadNestedRelations(\n parentRelation: string,\n remainingPaths: string[],\n constraints: RelationConstraints,\n ): Promise<void> {\n // Collect all loaded related models from the parent relation\n const relatedModels: Model[] = [];\n\n for (const model of this.models) {\n const loaded = this.getLoadedRelation(model, parentRelation);\n\n if (Array.isArray(loaded)) {\n relatedModels.push(...loaded);\n } else if (loaded) {\n relatedModels.push(loaded);\n }\n }\n\n if (relatedModels.length === 0) {\n return;\n }\n\n // Get the related model class\n const parentDefinition = this.getRelationDefinition(parentRelation);\n if (!parentDefinition) return;\n\n const RelatedModelClass = this.resolveModelClass(parentDefinition.model);\n\n // Create a new loader for the nested relation\n const nestedLoader = new RelationLoader(relatedModels, RelatedModelClass as ChildModel<Model>);\n\n // Load the next level\n await nestedLoader.load(remainingPaths, constraints);\n }\n\n // ==========================================================================\n // PRIVATE METHODS - UTILITIES\n // ==========================================================================\n\n /**\n * Parses a dot-notation relation path into segments.\n *\n * @param path - The relation path (e.g., \"posts.comments.author\")\n * @returns An array of relation names\n */\n private parseNestedRelation(path: string): string[] {\n return path.split(\".\");\n }\n\n /**\n * Resolves a model class from the registry by name.\n *\n * @param name - The registered model name\n * @returns The model class constructor\n * @throws Error if the model is not found in the registry\n */\n private resolveModelClass(\n model: string | ChildModel<Model> | Lazy<unknown>,\n ): ChildModel<Model> {\n if (typeof model === \"function\") return model;\n if (isLazy(model)) return model.resolve() as ChildModel<Model>;\n\n const ModelClass = getModelFromRegistry(model);\n\n if (!ModelClass) {\n const callerName = this.modelClass.name || \"unknown\";\n throw new Error(\n `Cannot resolve relation target — model \"${model}\" is not registered.\\n` +\n ` Caller: ${callerName} (relation target reference)\\n` +\n ` Common causes:\\n` +\n ` - The target model is missing the @RegisterModel() decorator\\n` +\n ` - The target model's module is not imported anywhere at startup\\n` +\n ` - Circular import between the caller and target — one of them sees\\n` +\n ` the other as undefined during module load\\n` +\n ` Fix: add an explicit \\`import \"<path-to-${model}-model>\";\\` to your app's\\n` +\n ` entry point so the decorator runs before any query consults this relation.\\n` +\n ` Alternative: declare the relation with \\`lazy(() => ${model})\\` instead of a string\\n` +\n ` to bind directly to the class (no registry lookup needed).`,\n );\n }\n\n return ModelClass;\n }\n\n /**\n * Applies the relation definition's `select` columns to the related query.\n * Runs before the constraint callback so an explicit constraint can still\n * override it. The key columns used to match records back to their owners\n * are always included.\n */\n private applySelect(\n query: { select: (columns: string[]) => unknown },\n definition: RelationDefinition,\n requiredKeys: string[],\n ): void {\n if (!definition.select || definition.select.length === 0) return;\n\n query.select([...new Set([...definition.select, ...requiredKeys])]);\n }\n\n /**\n * Gets the relation definition from the model class.\n *\n * @param name - The relation name\n * @returns The relation definition or undefined\n */\n private getRelationDefinition(name: string): RelationDefinition | undefined {\n const relations = (\n this.modelClass as unknown as { relations?: Record<string, RelationDefinition> }\n ).relations;\n return relations?.[name];\n }\n\n /**\n * Collects unique key values from all models.\n *\n * @param key - The key to collect values for\n * @returns An array of unique key values\n */\n private collectKeyValues(key: string): unknown[] {\n const values = new Set<unknown>();\n\n for (const model of this.models) {\n const value = model.get(key);\n if (value !== undefined && value !== null) {\n values.add(value);\n }\n }\n\n return [...values];\n }\n\n /**\n * Groups records by a key value.\n *\n * @param records - The records to group\n * @param key - The key to group by\n * @returns A map of key values to records\n */\n private groupBy(records: Model[], key: string): Map<unknown, Model[]> {\n const groups = new Map<unknown, Model[]>();\n\n for (const record of records) {\n const keyValue = record.get(key);\n\n if (!groups.has(keyValue)) {\n groups.set(keyValue, []);\n }\n\n groups.get(keyValue)!.push(record);\n }\n\n return groups;\n }\n\n /**\n * Sets a relation value on all models using a getter function.\n *\n * @param name - The relation name\n * @param getter - Function that returns the relation value for each model\n */\n private setRelationOnModels(name: string, getter: (model: TModel) => LoadedRelationResult): void {\n for (const model of this.models) {\n const value = getter(model);\n this.setLoadedRelation(model, name, value);\n }\n }\n\n /**\n * Gets a loaded relation from a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @returns The loaded relation value or undefined\n */\n private getLoadedRelation(model: TModel, name: string): LoadedRelationResult | undefined {\n const loadedRelations = (\n model as unknown as { loadedRelations?: Map<string, LoadedRelationResult> }\n ).loadedRelations;\n return loadedRelations?.get(name);\n }\n\n /**\n * Sets a loaded relation on a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @param value - The relation value\n */\n private setLoadedRelation(model: TModel, name: string, value: LoadedRelationResult): void {\n attachLoadedRelation(model as object, name, value);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"relation-loader.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/relations/relation-loader.ts"],"sourcesContent":["/**\n * @fileoverview Core relation loading logic for the Cascade ORM.\n *\n * The RelationLoader is responsible for efficiently loading related models\n * while preventing N+1 query problems through batch loading strategies.\n *\n * @module @warlock.js/cascade/relations/relation-loader\n */\n\nimport { isLazy, type Lazy } from \"@mongez/reinforcements\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { getModelFromRegistry, resolveModelName } from \"../model/register-model\";\nimport {\n inferBelongsToForeignKey,\n inferHasForeignKey,\n inferPivotKey,\n inferPivotTable,\n} from \"./key-conventions\";\nimport type {\n LoadedRelationResult,\n RelationConstraintCallback,\n RelationConstraints,\n RelationDefinition,\n} from \"./types\";\n\n// ============================================================================\n// SHARED HELPER — SINGLE SOURCE OF TRUTH FOR LOADED RELATIONS\n// ============================================================================\n\n/**\n * Attach a loaded relation onto a model instance, keeping the\n * `loadedRelations` Map and the direct property access in sync.\n *\n * The property is installed as a `defineProperty` getter/setter façade over\n * the Map — `model.posts` reads from `loadedRelations.get(\"posts\")`, and\n * assigning `model.posts = newPosts` writes back to the Map. Eliminates the\n * historical drift between the two storage slots: any mutation visible via\n * one path is visible via the other.\n *\n * Used by both `RelationLoader` (the `with()` path) and the Postgres\n * driver's `attachJoinedRelations` (the `joinWith()` path).\n *\n * @example\n * attachLoadedRelation(user, \"posts\", postsArray);\n * user.posts === user.loadedRelations.get(\"posts\"); // always true\n */\nexport function attachLoadedRelation(\n model: object,\n name: string,\n value: LoadedRelationResult,\n): void {\n const modelWithRelations = model as {\n loadedRelations?: Map<string, LoadedRelationResult>;\n };\n\n if (!modelWithRelations.loadedRelations) {\n modelWithRelations.loadedRelations = new Map();\n }\n\n const relations = modelWithRelations.loadedRelations;\n relations.set(name, value);\n\n // Install a getter/setter on the instance so direct property access reads\n // from (and writes to) the Map. `configurable: true` lets a later reload\n // re-define the property; `enumerable: true` so JSON.stringify and\n // Object.keys see the relation.\n Object.defineProperty(model, name, {\n configurable: true,\n enumerable: true,\n get(): LoadedRelationResult | undefined {\n return relations.get(name);\n },\n set(next: LoadedRelationResult): void {\n relations.set(name, next);\n },\n });\n}\n\n// ============================================================================\n// RELATION LOADER CLASS\n// ============================================================================\n\n/**\n * Efficiently loads relationships for one or more model instances.\n *\n * The RelationLoader handles:\n * - Batch loading to prevent N+1 query problems\n * - Nested relation loading via dot notation\n * - Constrained loading with query callbacks\n * - All relation types (hasOne, hasMany, belongsTo, belongsToMany)\n *\n * @template TModel - The type of model being loaded\n *\n * @example\n * ```typescript\n * const users = await User.query().get();\n * const loader = new RelationLoader(users, User);\n *\n * // Load single relation\n * await loader.load(\"posts\");\n *\n * // Load multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // Load nested relations\n * await loader.load(\"posts.comments.author\");\n *\n * // Load with constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"isPublished\", true),\n * });\n * ```\n */\nexport class RelationLoader<TModel extends Model = Model> {\n // ==========================================================================\n // PRIVATE PROPERTIES\n // ==========================================================================\n\n /**\n * The model instances to load relations for.\n */\n private readonly models: TModel[];\n\n /**\n * The model class constructor.\n */\n private readonly modelClass: ChildModel<TModel>;\n\n // ==========================================================================\n // CONSTRUCTOR\n // ==========================================================================\n\n /**\n * Creates a new RelationLoader instance.\n *\n * @param models - The model instances to load relations for\n * @param modelClass - The model class constructor\n */\n public constructor(models: TModel[], modelClass: ChildModel<TModel>) {\n this.models = models;\n this.modelClass = modelClass;\n }\n\n /**\n * Read the configured relation conventions from this model's data\n * source. Returns `undefined` when no overrides are set — the inference\n * helpers fall back to framework defaults in that case.\n */\n private get relationDefaults() {\n try {\n const dataSource = this.modelClass.getDataSource();\n\n if (!dataSource) return undefined;\n\n const namingConvention =\n dataSource.modelDefaults?.namingConvention ??\n dataSource.driver?.modelDefaults?.namingConvention;\n\n return { ...dataSource.relationDefaults, namingConvention };\n } catch {\n return undefined;\n }\n }\n\n // ==========================================================================\n // PUBLIC METHODS\n // ==========================================================================\n\n /**\n * Loads one or more relations for all model instances.\n *\n * @param relations - A single relation name, array of names, or constraints object\n * @param constraints - Optional constraints to apply to the loading query\n *\n * @example\n * ```typescript\n * // Single relation\n * await loader.load(\"posts\");\n *\n * // Multiple relations\n * await loader.load([\"posts\", \"organization\"]);\n *\n * // With constraints\n * await loader.load(\"posts\", {\n * posts: (query) => query.where(\"status\", \"active\"),\n * });\n * ```\n */\n public async load(\n relations: string | string[],\n constraints?: RelationConstraints,\n ): Promise<void> {\n // If no models, nothing to load\n if (this.models.length === 0) {\n return;\n }\n\n // Normalize to array\n const relationNames = Array.isArray(relations) ? relations : [relations];\n\n // Group the paths into a tree keyed by root relation, so a shared prefix\n // (\"posts.comments\", \"posts.tags\") is loaded ONCE and every child path is\n // loaded onto those same instances instead of overwriting them.\n const nodes = new Map<\n string,\n {\n constraint?: RelationConstraintCallback;\n children: string[];\n childConstraints: RelationConstraints;\n }\n >();\n\n for (const relationName of relationNames) {\n const [root, ...rest] = this.parseNestedRelation(relationName);\n const key = root ?? relationName;\n\n if (!nodes.has(key)) {\n nodes.set(key, { children: [], childConstraints: {} });\n }\n\n const node = nodes.get(key)!;\n const constraint = constraints?.[relationName];\n const callbackConstraint = typeof constraint === \"function\" ? constraint : undefined;\n\n if (rest.length === 0) {\n // A constraint belongs to the LAST segment of its path only.\n if (callbackConstraint) node.constraint = callbackConstraint;\n continue;\n }\n\n const childPath = rest.join(\".\");\n\n if (!node.children.includes(childPath)) {\n node.children.push(childPath);\n }\n\n if (callbackConstraint) node.childConstraints[childPath] = callbackConstraint;\n }\n\n for (const [root, node] of nodes) {\n await this.loadRelation(root, node.constraint, node.children, node.childConstraints);\n }\n }\n\n // ==========================================================================\n // PRIVATE METHODS - RELATION LOADING\n // ==========================================================================\n\n /**\n * Loads a single (root) relation once, then its child paths onto the same\n * loaded instances.\n *\n * @param name - The root relation name\n * @param constraint - Optional constraint callback, applied to this relation only\n * @param children - Remaining dot paths below this relation\n * @param childConstraints - Constraints keyed by the remaining dot paths\n */\n private async loadRelation(\n name: string,\n constraint?: RelationConstraintCallback,\n children: string[] = [],\n childConstraints: RelationConstraints = {},\n ): Promise<void> {\n const rootRelation: string | undefined = name || undefined;\n\n // An empty parse would otherwise reach `getRelationDefinition(undefined)`\n // and fail as `Relation \"undefined\" is not defined on model \"X\"` — a\n // message that sends the reader looking for a relation they never wrote,\n // instead of at the name they passed. Same failure, named correctly.\n if (rootRelation === undefined) {\n throw new Error(\n `Relation name \"${name}\" could not be parsed into a relation path on model ` +\n `\"${this.modelClass.name}\".`,\n );\n }\n\n // Get the relation definition from the model class\n const definition = this.getRelationDefinition(rootRelation);\n\n if (!definition) {\n throw new Error(\n `Relation \"${rootRelation}\" is not defined on model \"${this.modelClass.name}\". ` +\n `Make sure to define it in the static 'relations' property.`,\n );\n }\n\n // Load based on relation type\n switch (definition.type) {\n case \"hasMany\":\n await this.loadHasMany(rootRelation, definition, constraint);\n break;\n\n case \"hasOne\":\n await this.loadHasOne(rootRelation, definition, constraint);\n break;\n\n case \"belongsTo\":\n await this.loadBelongsTo(rootRelation, definition, constraint);\n break;\n\n case \"belongsToMany\":\n await this.loadBelongsToMany(rootRelation, definition, constraint);\n break;\n }\n\n // If there are nested relations, load them recursively\n if (children.length > 0) {\n await this.loadNestedRelations(rootRelation, children, childConstraints);\n }\n }\n\n /**\n * Loads a hasMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n // No values to query, set empty arrays\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Group by foreign key and assign to models\n const recordsByForeignKey = this.groupBy(relatedRecords as Model[], foreignKey);\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? [];\n });\n }\n\n /**\n * Loads a hasOne relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadHasOne(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const localKey = definition.localKey ?? this.modelClass.primaryKey ?? \"id\";\n const foreignKey =\n definition.foreignKey ?? inferHasForeignKey(this.modelClass.name, this.relationDefaults);\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(foreignKey, localKeyValues);\n\n this.applySelect(query, definition, [foreignKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by foreign key\n const recordsByForeignKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const fkValue = record.get(foreignKey);\n // For hasOne, take the first match only\n if (!recordsByForeignKey.has(fkValue)) {\n recordsByForeignKey.set(fkValue, record);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return recordsByForeignKey.get(keyValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsTo relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsTo(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const foreignKey =\n definition.foreignKey ?? inferBelongsToForeignKey(name, this.relationDefaults);\n const ownerKey = definition.localKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all foreign key values from the models\n const foreignKeyValues = this.collectKeyValues(foreignKey);\n\n if (foreignKeyValues.length === 0) {\n this.setRelationOnModels(name, () => null);\n return;\n }\n\n // Build and execute query\n const query = RelatedModel.query().whereIn(ownerKey, foreignKeyValues);\n\n this.applySelect(query, definition, [ownerKey]);\n\n if (constraint) {\n constraint(query);\n }\n\n const relatedRecords = await query.get();\n\n // Index by owner key\n const recordsByOwnerKey = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n const keyValue = record.get(ownerKey);\n recordsByOwnerKey.set(keyValue, record);\n }\n\n this.setRelationOnModels(name, (model) => {\n const fkValue = model.get(foreignKey);\n return recordsByOwnerKey.get(fkValue) ?? null;\n });\n }\n\n /**\n * Loads a belongsToMany relation for all models.\n *\n * @param name - The relation name\n * @param definition - The relation definition\n * @param constraint - Optional constraint callback\n */\n private async loadBelongsToMany(\n name: string,\n definition: RelationDefinition,\n constraint?: RelationConstraintCallback,\n ): Promise<void> {\n const RelatedModel = this.resolveModelClass(definition.model);\n const relatedModelName = resolveModelName(definition.model);\n const pivotTable =\n definition.pivot ??\n inferPivotTable(this.modelClass.name, relatedModelName, this.relationDefaults);\n const localKey = definition.pivotLocalKey ?? this.modelClass.primaryKey ?? \"id\";\n const pivotLocalKey =\n definition.localKey ?? inferPivotKey(this.modelClass.name, this.relationDefaults);\n const pivotForeignKey =\n definition.foreignKey ?? inferPivotKey(relatedModelName, this.relationDefaults);\n const relatedKey = definition.pivotForeignKey ?? RelatedModel.primaryKey ?? \"id\";\n\n // Collect all local key values\n const localKeyValues = this.collectKeyValues(localKey);\n\n if (localKeyValues.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 1: Query the pivot table to get the relationships\n const dataSource = this.modelClass.getDataSource();\n const pivotQuery = dataSource.driver\n .queryBuilder(pivotTable)\n .whereIn(pivotLocalKey, localKeyValues);\n\n const pivotRecords = (await pivotQuery.get()) as Record<string, unknown>[];\n\n if (pivotRecords.length === 0) {\n this.setRelationOnModels(name, () => []);\n return;\n }\n\n // Step 2: Collect related model IDs from pivot\n const relatedIds = [...new Set(pivotRecords.map((p) => p[pivotForeignKey]))];\n\n // Step 3: Query the related model\n const relatedQuery = RelatedModel.query().whereIn(relatedKey, relatedIds);\n\n this.applySelect(relatedQuery, definition, [relatedKey]);\n\n if (constraint) {\n constraint(relatedQuery);\n }\n\n const relatedRecords = await relatedQuery.get();\n\n // Step 4: Index related records by their key\n const relatedById = new Map<unknown, Model>();\n for (const record of relatedRecords as Model[]) {\n relatedById.set(record.get(relatedKey), record);\n }\n\n // Step 5: Build the relationship map from pivot data\n const relationshipMap = new Map<unknown, Model[]>();\n\n for (const pivot of pivotRecords) {\n const localValue = pivot[pivotLocalKey];\n const foreignValue = pivot[pivotForeignKey];\n const relatedRecord = relatedById.get(foreignValue);\n\n if (relatedRecord) {\n if (!relationshipMap.has(localValue)) {\n relationshipMap.set(localValue, []);\n }\n relationshipMap.get(localValue)!.push(relatedRecord);\n }\n }\n\n this.setRelationOnModels(name, (model) => {\n const keyValue = model.get(localKey);\n return relationshipMap.get(keyValue) ?? [];\n });\n }\n\n // ==========================================================================\n // PRIVATE METHODS - NESTED RELATIONS\n // ==========================================================================\n\n /**\n * Loads nested relations recursively.\n *\n * @param parentRelation - The name of the parent relation\n * @param remainingPaths - The remaining dot paths to load below the parent\n * @param constraints - Constraints keyed by those remaining paths\n */\n private async loadNestedRelations(\n parentRelation: string,\n remainingPaths: string[],\n constraints: RelationConstraints,\n ): Promise<void> {\n // Collect all loaded related models from the parent relation\n const relatedModels: Model[] = [];\n\n for (const model of this.models) {\n const loaded = this.getLoadedRelation(model, parentRelation);\n\n if (Array.isArray(loaded)) {\n relatedModels.push(...loaded);\n } else if (loaded) {\n relatedModels.push(loaded);\n }\n }\n\n if (relatedModels.length === 0) {\n return;\n }\n\n // Get the related model class\n const parentDefinition = this.getRelationDefinition(parentRelation);\n if (!parentDefinition) return;\n\n const RelatedModelClass = this.resolveModelClass(parentDefinition.model);\n\n // Create a new loader for the nested relation\n const nestedLoader = new RelationLoader(relatedModels, RelatedModelClass as ChildModel<Model>);\n\n // Load the next level\n await nestedLoader.load(remainingPaths, constraints);\n }\n\n // ==========================================================================\n // PRIVATE METHODS - UTILITIES\n // ==========================================================================\n\n /**\n * Parses a dot-notation relation path into segments.\n *\n * @param path - The relation path (e.g., \"posts.comments.author\")\n * @returns An array of relation names\n */\n private parseNestedRelation(path: string): string[] {\n return path.split(\".\");\n }\n\n /**\n * Resolves a model class from the registry by name.\n *\n * @param name - The registered model name\n * @returns The model class constructor\n * @throws Error if the model is not found in the registry\n */\n private resolveModelClass(model: string | ChildModel<Model> | Lazy<unknown>): ChildModel<Model> {\n if (typeof model === \"function\") return model;\n if (isLazy(model)) return model.resolve() as ChildModel<Model>;\n\n const ModelClass = getModelFromRegistry(model);\n\n if (!ModelClass) {\n const callerName = this.modelClass.name || \"unknown\";\n throw new Error(\n `Cannot resolve relation target — model \"${model}\" is not registered.\\n` +\n ` Caller: ${callerName} (relation target reference)\\n` +\n ` Common causes:\\n` +\n ` - The target model is missing the @RegisterModel() decorator\\n` +\n ` - The target model's module is not imported anywhere at startup\\n` +\n ` - Circular import between the caller and target — one of them sees\\n` +\n ` the other as undefined during module load\\n` +\n ` Fix: add an explicit \\`import \"<path-to-${model}-model>\";\\` to your app's\\n` +\n ` entry point so the decorator runs before any query consults this relation.\\n` +\n ` Alternative: declare the relation with \\`lazy(() => ${model})\\` instead of a string\\n` +\n ` to bind directly to the class (no registry lookup needed).`,\n );\n }\n\n return ModelClass;\n }\n\n /**\n * Applies the relation definition's `select` columns to the related query.\n * Runs before the constraint callback so an explicit constraint can still\n * override it. The key columns used to match records back to their owners\n * are always included.\n */\n private applySelect(\n query: { select: (columns: string[]) => unknown },\n definition: RelationDefinition,\n requiredKeys: string[],\n ): void {\n if (!definition.select || definition.select.length === 0) return;\n\n query.select([...new Set([...definition.select, ...requiredKeys])]);\n }\n\n /**\n * Gets the relation definition from the model class.\n *\n * @param name - The relation name\n * @returns The relation definition or undefined\n */\n private getRelationDefinition(name: string): RelationDefinition | undefined {\n const relations = (\n this.modelClass as unknown as { relations?: Record<string, RelationDefinition> }\n ).relations;\n return relations?.[name];\n }\n\n /**\n * Collects unique key values from all models.\n *\n * @param key - The key to collect values for\n * @returns An array of unique key values\n */\n private collectKeyValues(key: string): unknown[] {\n const values = new Set<unknown>();\n\n for (const model of this.models) {\n const value = model.get(key);\n if (value !== undefined && value !== null) {\n values.add(value);\n }\n }\n\n return [...values];\n }\n\n /**\n * Groups records by a key value.\n *\n * @param records - The records to group\n * @param key - The key to group by\n * @returns A map of key values to records\n */\n private groupBy(records: Model[], key: string): Map<unknown, Model[]> {\n const groups = new Map<unknown, Model[]>();\n\n for (const record of records) {\n const keyValue = record.get(key);\n\n if (!groups.has(keyValue)) {\n groups.set(keyValue, []);\n }\n\n groups.get(keyValue)!.push(record);\n }\n\n return groups;\n }\n\n /**\n * Sets a relation value on all models using a getter function.\n *\n * @param name - The relation name\n * @param getter - Function that returns the relation value for each model\n */\n private setRelationOnModels(name: string, getter: (model: TModel) => LoadedRelationResult): void {\n for (const model of this.models) {\n const value = getter(model);\n this.setLoadedRelation(model, name, value);\n }\n }\n\n /**\n * Gets a loaded relation from a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @returns The loaded relation value or undefined\n */\n private getLoadedRelation(model: TModel, name: string): LoadedRelationResult | undefined {\n const loadedRelations = (\n model as unknown as { loadedRelations?: Map<string, LoadedRelationResult> }\n ).loadedRelations;\n return loadedRelations?.get(name);\n }\n\n /**\n * Sets a loaded relation on a model instance.\n *\n * @param model - The model instance\n * @param name - The relation name\n * @param value - The relation value\n */\n private setLoadedRelation(model: TModel, name: string, value: LoadedRelationResult): void {\n attachLoadedRelation(model as object, name, value);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"database-remover.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;;;;;;;;cAAa,eAAA,YAA2B,eAAA;EA+BnB;EAAA,iBA7BF,KAAA;EA6Ca;EAAA,iBA1Cb,IAAA;EA0CmC;EAAA,iBAvCnC,UAAA;EA2MT;EAAA,iBAxMS,MAAA;EAkPT;EAAA,iBA/OS,KAAA;EA+OE;EAAA,iBA5OF,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAgBb,OAAA,CAAQ,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,aAAA;;;;;;;;;;;;;;;;;;;;;UAoKpD,kBAAA;;;;;;;;;;;;UAsBA,iBAAA;;;;;;;;;UAoBA,WAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-remover.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"sourcesContent":["import type {\n DriverContract,\n UpdateOperations,\n} from \"../contracts/database-driver.contract\";\nimport type {\n RemoverContract,\n RemoverOptions,\n RemoverResult,\n} from \"../contracts/database-remover.contract\";\nimport type { OnDeletedEventContext } from \"../events/model-events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { afterCommit } from \"../transactions/after-commit\";\nimport type { DataSource } from \"./../data-source/data-source\";\n\n/**\n * Database remover service that orchestrates model deletion.\n *\n * Handles the complete deletion pipeline:\n * 1. Strategy resolution (options → model static → data source default)\n * 2. Validation (check if model is new, has primary key)\n * 3. Event emission (deleting, deleted)\n * 4. Driver execution (based on strategy: trash, permanent, or soft)\n * 5. Post-deletion cleanup (mark as new, reset state)\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * const result = await remover.destroy();\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"permanent\" | \"soft\"\n * ```\n */\nexport class DatabaseRemover implements RemoverContract {\n /** The model instance being deleted */\n private readonly model: Model;\n\n /** Model constructor reference */\n private readonly ctor: ChildModel<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 remover instance for a model.\n *\n * @param model - The model instance to delete\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * await remover.destroy();\n * ```\n */\n public constructor(model: Model) {\n this.model = model;\n this.ctor = model.constructor as ChildModel<Model>;\n this.dataSource = this.ctor.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = this.ctor.table;\n this.primaryKey = this.ctor.primaryKey;\n }\n\n /**\n * Destroy (delete) the model instance from the database.\n *\n * @param options - Remover options\n * @returns Result containing success status, strategy used, and metadata\n * @throws {Error} If model is new (not saved) or if deletion fails\n */\n public async destroy(options: RemoverOptions = {}): Promise<RemoverResult> {\n // 1. Resolve strategy (options → model static → data source default → permanent)\n const strategy =\n options.strategy ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\";\n\n // 2. Validate model is not new and has primary key\n if (this.model.isNew) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance that hasn't been saved to the database.`,\n );\n }\n\n // The value captured when the model became persisted, never the current\n // (mass-assignable) one — a delete must not be retargetable by a payload\n // carrying `{ id: \"<victim-id>\" }`. Same rule as the writer's update filter.\n const primaryKeyValue = this.model.trustedPrimaryKey;\n if (!primaryKeyValue) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance: primary key (${this.primaryKey}) is missing.`,\n );\n }\n\n // 3. Emit deleting event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleting\", {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n });\n }\n\n // 4. Execute deletion based on strategy\n let deletedCount = 0;\n let trashRecord: Record<string, unknown> | undefined;\n\n const filter = { [this.primaryKey]: primaryKeyValue };\n\n const context: Partial<OnDeletedEventContext> = {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n };\n\n switch (strategy) {\n case \"trash\": {\n // Move to trash table, then delete\n const trashTable = this.resolveTrashTable();\n const documentData = { ...this.model.data };\n\n // Prepare trash record with metadata and handle ID conflicts\n const trashData = this.prepareTrashRecord(documentData);\n\n // Insert into trash table\n const insertResult = await this.driver.insert(trashTable, trashData);\n trashRecord = insertResult.document as Record<string, unknown>;\n\n context.trashRecord = trashRecord;\n\n // Delete original\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"permanent\": {\n // Direct deletion\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"soft\": {\n // Set deletedAt timestamp (using resolved column name)\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n // Only proceed if deletedAtColumn is configured (not false or undefined)\n if (deletedAtColumn === false || deletedAtColumn === undefined) {\n throw new Error(\n `Cannot perform soft delete on ${this.ctor.name}: deletedAtColumn is not configured. ` +\n `Set a column name or use a different delete strategy.`,\n );\n }\n\n const deletedAt = new Date();\n const updateOperations: UpdateOperations = {\n $set: { [deletedAtColumn]: deletedAt },\n };\n // Guard: an already soft-deleted row must keep its original timestamp\n const softFilter = { ...filter, [deletedAtColumn]: null };\n const updateResult = await this.driver.update(\n this.table,\n softFilter,\n updateOperations,\n );\n deletedCount = updateResult.modifiedCount > 0 ? 1 : 0;\n\n // The row stays (unlike trash/permanent), so reflect the persisted\n // timestamp on the in-memory model — otherwise the instance is stale\n // and `model.get(deletedAtColumn)` stays undefined after destroy().\n if (deletedCount > 0) {\n this.model.set(deletedAtColumn, deletedAt);\n\n // The timestamp is already persisted, so it must not stay dirty.\n // Re-baseline only when it is the sole pending change, so unsaved\n // edits to other columns keep their dirty state.\n const dirty = this.model.dirtyTracker.getDirtyColumns();\n if (dirty.length === 1 && dirty[0] === deletedAtColumn) {\n this.model.dirtyTracker.reset(\n this.model.data as Record<string, unknown>,\n );\n }\n }\n break;\n }\n }\n\n if (deletedCount === 0) {\n throw new Error(\n `Failed to destroy ${this.ctor.name} instance: record not found.`,\n );\n }\n\n context.deletedCount = deletedCount;\n\n // 5. Post-deletion cleanup\n // Only mark as new for permanent and trash (soft delete keeps the record)\n if (strategy !== \"soft\") {\n this.model.isNew = true;\n }\n\n // 6. Emit deleted event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleted\", context);\n }\n\n // 7. Queue sync operations after COMMIT. A soft delete keeps the row, so\n // embedded copies must stay: only permanent/trash fire a delete sync.\n if (!options.skipSync && strategy !== \"soft\") {\n afterCommit(() => this.triggerSync());\n }\n\n return {\n success: true,\n deletedCount,\n strategy,\n trashRecord,\n };\n }\n\n /**\n * Prepare the trash record by preserving all original fields and adding deletion metadata.\n *\n * Keeps all original fields intact for easy restoration and adds:\n * - `deletedAt`: Timestamp when the record was deleted\n * - `originalTable`: The table/collection the record came from (for filtering in restoreAll)\n *\n * **ID Handling:**\n * - MongoDB with `_id`: Keeps `_id` as-is (unique across database)\n * - MongoDB with auto-increment `id`: Keeps `id` as a regular field (not primary key)\n * - SQL: Keeps original `id` as a regular field (trash table uses its own auto-increment primary key)\n *\n * The trash table should use its own primary key structure:\n * - MongoDB: Uses `_id` (ObjectId) as primary key, original `id` is just a field\n * - SQL: Uses auto-increment `trashId` as primary key, original `id` is just a field\n *\n * @param documentData - The original document data\n * @returns Prepared trash record data with all original fields + deletedAt + originalTable\n * @private\n */\n private prepareTrashRecord(\n documentData: Record<string, unknown>,\n ): Record<string, unknown> {\n // Preserve all original fields and add deletion metadata\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n return {\n ...documentData,\n [typeof deletedAtColumn === \"string\" ? deletedAtColumn : \"deletedAt\"]:\n new Date(),\n originalTable: this.table,\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 * Trigger sync operations after successful deletion.\n *\n * Emits a model.deleted event that ModelSyncOperation listens to.\n * The sync is handled by registered sync operations, not directly here.\n *\n * @private\n */\n private triggerSync(): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"delete\",\n execute: () => triggerModelEvent(this.ctor, \"deleted\", this.model),\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database-remover.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/remover/database-remover.ts"],"sourcesContent":["import type { DriverContract, UpdateOperations } from \"../contracts/database-driver.contract\";\nimport type {\n RemoverContract,\n RemoverOptions,\n RemoverResult,\n} from \"../contracts/database-remover.contract\";\nimport type { OnDeletedEventContext } from \"../events/model-events\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport { afterCommit } from \"../transactions/after-commit\";\nimport type { DataSource } from \"./../data-source/data-source\";\n\n/**\n * Database remover service that orchestrates model deletion.\n *\n * Handles the complete deletion pipeline:\n * 1. Strategy resolution (options → model static → data source default)\n * 2. Validation (check if model is new, has primary key)\n * 3. Event emission (deleting, deleted)\n * 4. Driver execution (based on strategy: trash, permanent, or soft)\n * 5. Post-deletion cleanup (mark as new, reset state)\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * const result = await remover.destroy();\n *\n * console.log(result.success); // true\n * console.log(result.strategy); // \"trash\" | \"permanent\" | \"soft\"\n * ```\n */\nexport class DatabaseRemover implements RemoverContract {\n /** The model instance being deleted */\n private readonly model: Model;\n\n /** Model constructor reference */\n private readonly ctor: ChildModel<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 remover instance for a model.\n *\n * @param model - The model instance to delete\n *\n * @example\n * ```typescript\n * const user = await User.find(1);\n * const remover = new DatabaseRemover(user);\n * await remover.destroy();\n * ```\n */\n public constructor(model: Model) {\n this.model = model;\n this.ctor = model.constructor as ChildModel<Model>;\n this.dataSource = this.ctor.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = this.ctor.table;\n this.primaryKey = this.ctor.primaryKey;\n }\n\n /**\n * Destroy (delete) the model instance from the database.\n *\n * @param options - Remover options\n * @returns Result containing success status, strategy used, and metadata\n * @throws {Error} If model is new (not saved) or if deletion fails\n */\n public async destroy(options: RemoverOptions = {}): Promise<RemoverResult> {\n // 1. Resolve strategy (options → model static → data source default → permanent)\n const strategy =\n options.strategy ??\n this.ctor.deleteStrategy ??\n this.dataSource.defaultDeleteStrategy ??\n \"permanent\";\n\n // 2. Validate model is not new and has primary key\n if (this.model.isNew) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance that hasn't been saved to the database.`,\n );\n }\n\n // The value captured when the model became persisted, never the current\n // (mass-assignable) one — a delete must not be retargetable by a payload\n // carrying `{ id: \"<victim-id>\" }`. Same rule as the writer's update filter.\n const primaryKeyValue = this.model.trustedPrimaryKey;\n if (!primaryKeyValue) {\n throw new Error(\n `Cannot destroy ${this.ctor.name} instance: primary key (${this.primaryKey}) is missing.`,\n );\n }\n\n // 3. Emit deleting event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleting\", {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n });\n }\n\n // 4. Execute deletion based on strategy\n let deletedCount = 0;\n let trashRecord: Record<string, unknown> | undefined;\n\n const filter = { [this.primaryKey]: primaryKeyValue };\n\n const context: Partial<OnDeletedEventContext> = {\n strategy,\n primaryKeyValue,\n primaryKey: this.primaryKey,\n };\n\n switch (strategy) {\n case \"trash\": {\n // Move to trash table, then delete\n const trashTable = this.resolveTrashTable();\n const documentData = { ...this.model.data };\n\n // Prepare trash record with metadata and handle ID conflicts\n const trashData = this.prepareTrashRecord(documentData);\n\n // Insert into trash table\n const insertResult = await this.driver.insert(trashTable, trashData);\n trashRecord = insertResult.document as Record<string, unknown>;\n\n context.trashRecord = trashRecord;\n\n // Delete original\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"permanent\": {\n // Direct deletion\n const result = await this.driver.delete(this.table, filter);\n deletedCount = result > 0 ? 1 : 0;\n break;\n }\n\n case \"soft\": {\n // Set deletedAt timestamp (using resolved column name)\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n // Only proceed if deletedAtColumn is configured (not false or undefined)\n if (deletedAtColumn === false || deletedAtColumn === undefined) {\n throw new Error(\n `Cannot perform soft delete on ${this.ctor.name}: deletedAtColumn is not configured. ` +\n `Set a column name or use a different delete strategy.`,\n );\n }\n\n const deletedAt = new Date();\n const updateOperations: UpdateOperations = {\n $set: { [deletedAtColumn]: deletedAt },\n };\n // Guard: an already soft-deleted row must keep its original timestamp\n const softFilter = { ...filter, [deletedAtColumn]: null };\n const updateResult = await this.driver.update(this.table, softFilter, updateOperations);\n deletedCount = updateResult.modifiedCount > 0 ? 1 : 0;\n\n // The row stays (unlike trash/permanent), so reflect the persisted\n // timestamp on the in-memory model — otherwise the instance is stale\n // and `model.get(deletedAtColumn)` stays undefined after destroy().\n if (deletedCount > 0) {\n this.model.set(deletedAtColumn, deletedAt);\n\n // The timestamp is already persisted, so it must not stay dirty.\n // Re-baseline only when it is the sole pending change, so unsaved\n // edits to other columns keep their dirty state.\n const dirty = this.model.dirtyTracker.getDirtyColumns();\n if (dirty.length === 1 && dirty[0] === deletedAtColumn) {\n this.model.dirtyTracker.reset(this.model.data as Record<string, unknown>);\n }\n }\n break;\n }\n }\n\n if (deletedCount === 0) {\n throw new Error(`Failed to destroy ${this.ctor.name} instance: record not found.`);\n }\n\n context.deletedCount = deletedCount;\n\n // 5. Post-deletion cleanup\n // Only mark as new for permanent and trash (soft delete keeps the record)\n if (strategy !== \"soft\") {\n this.model.isNew = true;\n }\n\n // 6. Emit deleted event (unless skipEvents)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"deleted\", context);\n }\n\n // 7. Queue sync operations after COMMIT. A soft delete keeps the row, so\n // embedded copies must stay: only permanent/trash fire a delete sync.\n if (!options.skipSync && strategy !== \"soft\") {\n afterCommit(() => this.triggerSync());\n }\n\n return {\n success: true,\n deletedCount,\n strategy,\n trashRecord,\n };\n }\n\n /**\n * Prepare the trash record by preserving all original fields and adding deletion metadata.\n *\n * Keeps all original fields intact for easy restoration and adds:\n * - `deletedAt`: Timestamp when the record was deleted\n * - `originalTable`: The table/collection the record came from (for filtering in restoreAll)\n *\n * **ID Handling:**\n * - MongoDB with `_id`: Keeps `_id` as-is (unique across database)\n * - MongoDB with auto-increment `id`: Keeps `id` as a regular field (not primary key)\n * - SQL: Keeps original `id` as a regular field (trash table uses its own auto-increment primary key)\n *\n * The trash table should use its own primary key structure:\n * - MongoDB: Uses `_id` (ObjectId) as primary key, original `id` is just a field\n * - SQL: Uses auto-increment `trashId` as primary key, original `id` is just a field\n *\n * @param documentData - The original document data\n * @returns Prepared trash record data with all original fields + deletedAt + originalTable\n * @private\n */\n private prepareTrashRecord(documentData: Record<string, unknown>): Record<string, unknown> {\n // Preserve all original fields and add deletion metadata\n const deletedAtColumn = this.ctor.deletedAtColumn;\n\n return {\n ...documentData,\n [typeof deletedAtColumn === \"string\" ? deletedAtColumn : \"deletedAt\"]: new Date(),\n originalTable: this.table,\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 * Trigger sync operations after successful deletion.\n *\n * Emits a model.deleted event that ModelSyncOperation listens to.\n * The sync is handled by registered sync operations, not directly here.\n *\n * @private\n */\n private triggerSync(): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"delete\",\n execute: () => triggerModelEvent(this.ctor, \"deleted\", this.model),\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-context.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-context.ts"],"mappings":";;;;AAYmC;AAKnC;;cALa,sBAAA;;;;cAKA,kBAAA;
|
|
1
|
+
{"version":3,"file":"sync-context.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-context.ts"],"mappings":";;;;AAYmC;AAKnC;;cALa,sBAAA;;;;cAKA,kBAAA;EAQ+C;;;;;;;EAAA,OAA5C,aAAA,CAAc,WAAA,EAAa,eAAA,EAAiB,aAAA,WAAwB,WAAW;EA8BxF;;;;;;;;;;EAAA,OANS,QAAA,CACZ,KAAA,UACA,KAAA,YACA,WAAA,UACA,QAAA,UACA,eAAA;IACG,KAAA;IAAgB,KAAA;EAAA;EA2D6C;;;;;;;EAAA,OAhCpD,QAAA,CAAS,KAAA,YAAiB,WAAA;;;;;;;;SAW1B,WAAA,CAAY,KAAA,YAAiB,SAAA;;;;;;;SAU7B,WAAA,CAAY,KAAA;;;;;;;;SAWZ,aAAA,CAAc,YAAA,UAAsB,QAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-context.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-context.ts"],"sourcesContent":["/**\n * Sync context management and validation.\n *\n * @module cascade-next/sync/sync-context\n */\n\nimport type { SyncContext, SyncInstruction } from \"./types\";\n\n/**\n * Default maximum sync depth.\n * Prevents infinite sync chains.\n */\nexport const DEFAULT_MAX_SYNC_DEPTH = 3;\n\n/**\n * Manages sync context and provides validation utilities.\n */\nexport class SyncContextManager {\n /**\n * Creates a new sync context from a sync instruction.\n *\n * @param instruction - The sync instruction to create context from\n * @param affectedCount - Number of documents affected\n * @returns A new sync context\n */\n public static createContext(
|
|
1
|
+
{"version":3,"file":"sync-context.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-context.ts"],"sourcesContent":["/**\n * Sync context management and validation.\n *\n * @module cascade-next/sync/sync-context\n */\n\nimport type { SyncContext, SyncInstruction } from \"./types\";\n\n/**\n * Default maximum sync depth.\n * Prevents infinite sync chains.\n */\nexport const DEFAULT_MAX_SYNC_DEPTH = 3;\n\n/**\n * Manages sync context and provides validation utilities.\n */\nexport class SyncContextManager {\n /**\n * Creates a new sync context from a sync instruction.\n *\n * @param instruction - The sync instruction to create context from\n * @param affectedCount - Number of documents affected\n * @returns A new sync context\n */\n public static createContext(instruction: SyncInstruction, affectedCount: number): SyncContext {\n return {\n sourceModel: instruction.sourceModel,\n sourceId: instruction.sourceId,\n currentDepth: instruction.depth,\n syncChain: [...instruction.chain],\n targetModel: instruction.targetModel,\n filter: { ...instruction.filter },\n update: { ...instruction.update },\n affectedCount,\n timestamp: new Date(),\n };\n }\n\n /**\n * Validates if a sync operation can proceed based on depth and cycle detection.\n *\n * @param depth - Current sync depth\n * @param chain - Current sync chain\n * @param targetModel - Target model name\n * @param maxDepth - Maximum allowed depth\n * @param preventCircular - Whether to prevent circular references\n * @returns Validation result with success flag and optional error message\n */\n public static validate(\n depth: number,\n chain: string[],\n targetModel: string,\n maxDepth: number,\n preventCircular: boolean,\n ): { valid: boolean; error?: string } {\n // Check depth limit\n if (depth > maxDepth) {\n return {\n valid: false,\n error: `Sync depth limit exceeded: ${depth} > ${maxDepth}. Chain: ${chain.join(\" → \")}`,\n };\n }\n\n // Check for circular references\n if (preventCircular && this.hasCycle(chain, targetModel)) {\n return {\n valid: false,\n error: `Circular sync detected: ${targetModel} already exists in chain [${chain.join(\" → \")}]`,\n };\n }\n\n return { valid: true };\n }\n\n /**\n * Checks if adding a target model would create a cycle in the sync chain.\n *\n * @param chain - Current sync chain\n * @param targetModel - Model to be added to the chain\n * @returns True if adding the model would create a cycle\n */\n public static hasCycle(chain: string[], targetModel: string): boolean {\n return chain.includes(targetModel);\n }\n\n /**\n * Creates a new sync chain by appending a model name.\n *\n * @param chain - Current sync chain\n * @param modelName - Model name to append\n * @returns New sync chain array\n */\n public static extendChain(chain: string[], modelName: string): string[] {\n return [...chain, modelName];\n }\n\n /**\n * Formats a sync chain for display.\n *\n * @param chain - Sync chain to format\n * @returns Formatted string (e.g., \"Category → Product → Module\")\n */\n public static formatChain(chain: string[]): string {\n return chain.join(\" → \");\n }\n\n /**\n * Checks if the current depth allows for further syncing.\n *\n * @param currentDepth - Current depth in the chain\n * @param maxDepth - Maximum allowed depth\n * @returns True if more syncing is allowed\n */\n public static canSyncDeeper(currentDepth: number, maxDepth: number): boolean {\n return currentDepth < maxDepth;\n }\n}\n"],"mappings":";;;;;AAYA,MAAa,yBAAyB;;;;AAKtC,IAAa,qBAAb,MAAgC;;;;;;;;CAQ9B,OAAc,cAAc,aAA8B,eAAoC;EAC5F,OAAO;GACL,aAAa,YAAY;GACzB,UAAU,YAAY;GACtB,cAAc,YAAY;GAC1B,WAAW,CAAC,GAAG,YAAY,KAAK;GAChC,aAAa,YAAY;GACzB,QAAQ,EAAE,GAAG,YAAY,OAAO;GAChC,QAAQ,EAAE,GAAG,YAAY,OAAO;GAChC;GACA,2BAAW,IAAI,KAAK;EACtB;CACF;;;;;;;;;;;CAYA,OAAc,SACZ,OACA,OACA,aACA,UACA,iBACoC;EAEpC,IAAI,QAAQ,UACV,OAAO;GACL,OAAO;GACP,OAAO,8BAA8B,MAAM,KAAK,SAAS,WAAW,MAAM,KAAK,KAAK;EACtF;EAIF,IAAI,mBAAmB,KAAK,SAAS,OAAO,WAAW,GACrD,OAAO;GACL,OAAO;GACP,OAAO,2BAA2B,YAAY,4BAA4B,MAAM,KAAK,KAAK,EAAE;EAC9F;EAGF,OAAO,EAAE,OAAO,KAAK;CACvB;;;;;;;;CASA,OAAc,SAAS,OAAiB,aAA8B;EACpE,OAAO,MAAM,SAAS,WAAW;CACnC;;;;;;;;CASA,OAAc,YAAY,OAAiB,WAA6B;EACtE,OAAO,CAAC,GAAG,OAAO,SAAS;CAC7B;;;;;;;CAQA,OAAc,YAAY,OAAyB;EACjD,OAAO,MAAM,KAAK,KAAK;CACzB;;;;;;;;CASA,OAAc,cAAc,cAAsB,UAA2B;EAC3E,OAAO,eAAe;CACxB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-fanout.mjs","names":[],"sources":["../../../../../../../../packages/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
|
|
1
|
+
{"version":3,"file":"sync-fanout.mjs","names":[],"sources":["../../../../../../../../packages/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 =\n options.reportFailure ??\n ((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,gBACH,QAAQ,mBACN,KAAK,UAAU;GACf,IAAI,MACF,YACA,eACA,aAAa,IAAI,UAAU,mBAAmB,IAAI,YAAY,SAAS,KAAK,YAAY,aAAa,OACvG;EACF;CACJ;;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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-manager.d.mts","names":[],"sources":["../../../../../../../../packages/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":["../../../../../../../../packages/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,QAiDA,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-manager.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-manager.ts"],"sourcesContent":["/**\n * Sync manager service for handling multi-level sync operations.\n *\n * @module cascade-next/sync/sync-manager\n */\n\nimport type { DriverContract } from \"../contracts/database-driver.contract\";\nimport { type ChildModel, Model } from \"../model/model\";\nimport { DEFAULT_MAX_SYNC_DEPTH, SyncContextManager } from \"./sync-context\";\nimport type {\n CollectInstructionsPayload,\n SyncConfig,\n SyncEventPayload,\n SyncInstruction,\n SyncInstructionOptions,\n SyncResult,\n} from \"./types\";\n\n/**\n * Manages sync operations across models with multi-level support.\n */\nexport class SyncManager {\n private readonly sourceModel: ChildModel<Model>;\n private readonly driver: DriverContract;\n\n /**\n * Creates a new sync manager.\n *\n * @param sourceModel - The source model class\n * @param driver - The database driver\n */\n public constructor(sourceModel: ChildModel<Model>, driver: DriverContract) {\n this.sourceModel = sourceModel;\n this.driver = driver;\n }\n\n /**\n * Executes sync operations for a model update.\n *\n * @param sourceId - The source model ID\n * @param updatedData - The updated data to sync (Model instance or plain data)\n * @param changedFields - Fields that were changed (for filtering)\n * @returns Sync result with success status and details\n */\n public async syncUpdate(\n sourceId: string | number,\n updatedData: Record<string, unknown> | Model,\n changedFields: string[],\n ): Promise<SyncResult> {\n try {\n const syncConfigs = this.getSyncConfigs();\n\n if (syncConfigs.length === 0) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: DEFAULT_MAX_SYNC_DEPTH,\n preventCircular: true,\n };\n\n const instructions = await this.collectInstructions({\n sourceId,\n updatedData,\n changedFields,\n syncConfigs,\n options,\n });\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n // Catch-all error handler for unexpected failures\n console.error(`Sync update failed for ${this.sourceModel.name}#${sourceId}:`, error);\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync operations for a model update with a specific config.\n * Used by ModelSyncOperation for event-based sync.\n *\n * @param sourceId - The source model ID\n * @param updatedData - The updated data to sync\n * @param changedFields - Fields that were changed\n * @param config - The sync configuration to use\n * @returns Sync result with success status and details\n */\n public async syncUpdateWithConfig(\n sourceId: string | number,\n updatedData: Record<string, unknown> | Model,\n changedFields: string[],\n config: SyncConfig,\n ): Promise<SyncResult> {\n try {\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: config.maxSyncDepth,\n preventCircular: config.preventCircularSync,\n };\n\n const instructions = await this.collectInstructions({\n sourceId,\n updatedData,\n changedFields,\n syncConfigs: [config],\n options,\n });\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n console.error(\n `Sync update with config failed for ${this.sourceModel.name}#${sourceId}:`,\n error,\n );\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync delete operations with a specific config.\n * Used by ModelSyncOperation for event-based sync.\n *\n * @param sourceId - The source model ID\n * @param config - The sync configuration to use\n * @returns Sync result with success status and details\n */\n public async syncDeleteWithConfig(\n sourceId: string | number,\n config: SyncConfig,\n ): Promise<SyncResult> {\n try {\n if (!config.unsetOnDelete) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: config.maxSyncDepth,\n preventCircular: config.preventCircularSync,\n };\n\n const instructions = await this.collectDeleteInstructions(sourceId, [config], options);\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n console.error(\n `Sync delete with config failed for ${this.sourceModel.name}#${sourceId}:`,\n error,\n );\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync operations for a model deletion.\n *\n * @param sourceId - The source model ID\n * @returns Sync result with success status and details\n */\n public async syncDelete(sourceId: string | number): Promise<SyncResult> {\n try {\n const syncConfigs = this.getSyncConfigs();\n\n if (syncConfigs.length === 0) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: DEFAULT_MAX_SYNC_DEPTH,\n preventCircular: true,\n };\n\n const instructions = await this.collectDeleteInstructions(sourceId, syncConfigs, options);\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n // Catch-all error handler for unexpected failures\n console.error(`Sync delete failed for ${this.sourceModel.name}#${sourceId}:`, error);\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Collects sync instructions recursively with depth limiting.\n *\n * @param payload - Data payload\n * @returns Array of sync instructions\n */\n private async collectInstructions(\n payload: CollectInstructionsPayload,\n ): Promise<SyncInstruction[]> {\n const { sourceId, updatedData, changedFields, syncConfigs, options } = payload;\n const instructions: SyncInstruction[] = [];\n\n for (const config of syncConfigs) {\n // Check if we should sync based on watched fields\n if (!this.shouldSync(config, changedFields)) {\n continue;\n }\n\n // Validate depth and circular references\n const validation = SyncContextManager.validate(\n options.currentDepth,\n options.syncChain,\n config.targetModelClass.name,\n config.maxSyncDepth,\n config.preventCircularSync,\n );\n\n if (!validation.valid) {\n console.warn(`Sync validation failed: ${validation.error}`);\n continue;\n }\n\n // Get embedded data (handle both Model instance and plain data)\n const embedData = await this.getEmbedData(updatedData, config);\n\n // Build instruction\n const instruction = this.buildUpdateInstruction(sourceId, config, embedData, options);\n\n instructions.push(instruction);\n\n // Emit syncing event\n await this.emitSyncingEvent(instruction);\n\n }\n\n return instructions;\n }\n\n /**\n * Collects delete sync instructions.\n *\n * @param sourceId - Source model ID\n * @param syncConfigs - Sync configurations\n * @param options - Instruction options\n * @returns Array of sync instructions\n */\n private async collectDeleteInstructions(\n sourceId: string | number,\n syncConfigs: SyncConfig[],\n options: SyncInstructionOptions,\n ): Promise<SyncInstruction[]> {\n const instructions: SyncInstruction[] = [];\n\n for (const config of syncConfigs) {\n if (!config.unsetOnDelete) {\n continue;\n }\n\n const validation = SyncContextManager.validate(\n options.currentDepth,\n options.syncChain,\n config.targetModelClass.name,\n config.maxSyncDepth,\n config.preventCircularSync,\n );\n\n if (!validation.valid) {\n continue;\n }\n\n const instruction = this.buildDeleteInstruction(sourceId, config, options);\n instructions.push(instruction);\n\n await this.emitSyncingEvent(instruction);\n }\n\n return instructions;\n }\n\n /**\n * Builds an update sync instruction.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @param embedData - Embedded data to sync\n * @param options - Instruction options\n * @returns Sync instruction\n */\n private buildUpdateInstruction(\n sourceId: string | number,\n config: SyncConfig,\n embedData: Record<string, unknown>,\n options: SyncInstructionOptions,\n ): SyncInstruction {\n const targetModelClass = config.targetModelClass;\n const filter = this.buildFilter(sourceId, config);\n const update = this.buildUpdate(embedData, config);\n\n const instruction: SyncInstruction = {\n targetTable: targetModelClass.table,\n targetModel: targetModelClass.name,\n filter,\n update,\n depth: options.currentDepth,\n chain: [...options.syncChain],\n sourceModel: this.sourceModel.name,\n sourceId,\n };\n\n // Add array update metadata if needed\n if (config.isMany) {\n instruction.isArrayUpdate = true;\n instruction.arrayField = config.targetField;\n instruction.identifierField = config.identifierField;\n instruction.identifierValue = sourceId;\n }\n\n return instruction;\n }\n\n /**\n * Builds a delete sync instruction.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @param options - Instruction options\n * @returns Sync instruction\n */\n private buildDeleteInstruction(\n sourceId: string | number,\n config: SyncConfig,\n options: SyncInstructionOptions,\n ): SyncInstruction {\n const targetModelClass = config.targetModelClass;\n const filter = this.buildFilter(sourceId, config);\n // For arrays, remove only the deleted element instead of wiping the whole field\n const update = config.isMany\n ? { $pull: { [config.targetField]: { [config.identifierField]: sourceId } } }\n : { $unset: { [config.targetField]: 1 } };\n\n return {\n targetTable: targetModelClass.table,\n targetModel: targetModelClass.name,\n filter,\n update,\n depth: options.currentDepth,\n chain: [...options.syncChain],\n sourceModel: this.sourceModel.name,\n sourceId,\n };\n }\n\n /**\n * Builds a filter for identifying target documents.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @returns Filter object\n */\n private buildFilter(sourceId: string | number, config: SyncConfig): Record<string, unknown> {\n if (config.isMany) {\n // For arrays: { \"products.id\": productId }\n return { [`${config.targetField}.${config.identifierField}`]: sourceId };\n } else {\n // For single: { \"category.id\": categoryId }\n return { [`${config.targetField}.${config.identifierField}`]: sourceId };\n }\n }\n\n /**\n * Builds an update operation for syncing data.\n *\n * @param embedData - Embedded data to sync\n * @param config - Sync configuration\n * @returns Update operation object\n */\n private buildUpdate(\n embedData: Record<string, unknown>,\n config: SyncConfig,\n ): Record<string, unknown> {\n if (config.isMany) {\n // For arrays: use positional operator $\n return { $set: { [`${config.targetField}.$`]: embedData } };\n } else {\n // For single: direct set\n return { $set: { [config.targetField]: embedData } };\n }\n }\n\n /**\n * Executes sync instructions, once each, through the driver's dialect-aware\n * sync adapter. Instructions run in depth order and each one records its own\n * outcome, so one failing instruction never re-runs the ones that succeeded.\n *\n * @param instructions - Array of sync instructions\n * @returns Sync result\n */\n private async executeInstructions(instructions: SyncInstruction[]): Promise<SyncResult> {\n const result: SyncResult = {\n success: true,\n attempted: instructions.length,\n succeeded: 0,\n failed: 0,\n errors: [],\n depthReached: 0,\n contexts: [],\n };\n\n if (instructions.length === 0) {\n return result;\n }\n\n const adapter = this.driver.syncAdapter();\n\n for (const [depth, depthInstructions] of this.groupByDepth(instructions)) {\n result.depthReached = Math.max(result.depthReached, depth);\n\n for (const instruction of depthInstructions) {\n try {\n const affected = await adapter.executeBatch([instruction]);\n\n const context = SyncContextManager.createContext(instruction, affected);\n result.contexts.push(context);\n result.succeeded++;\n\n await this.emitSyncedEvent(context);\n } catch (error) {\n result.failed++;\n\n const errorMessage = this.formatSyncError(instruction, error);\n const syncError = new Error(errorMessage);\n\n // Preserve original error stack if available\n if (error instanceof Error && error.stack) {\n syncError.stack = error.stack;\n }\n\n result.errors.push({ instruction, error: syncError });\n\n console.error(`Sync operation failed:`, {\n sourceModel: instruction.sourceModel,\n sourceId: instruction.sourceId,\n targetModel: instruction.targetModel,\n targetTable: instruction.targetTable,\n depth: instruction.depth,\n chain: SyncContextManager.formatChain(instruction.chain),\n filter: instruction.filter,\n error: errorMessage,\n });\n }\n }\n }\n\n result.success = result.failed === 0;\n return result;\n }\n\n /**\n * Formats a sync error with detailed context.\n *\n * @param instruction - The failed instruction\n * @param error - The error that occurred\n * @returns Formatted error message\n */\n private formatSyncError(instruction: SyncInstruction, error: unknown): string {\n const baseMessage = error instanceof Error ? error.message : String(error);\n const chain = SyncContextManager.formatChain(instruction.chain);\n\n return [\n `Sync failed at depth ${instruction.depth}:`,\n `Chain: ${chain} → ${instruction.targetModel}`,\n `Source: ${instruction.sourceModel}#${instruction.sourceId}`,\n `Target: ${instruction.targetTable}`,\n `Error: ${baseMessage}`,\n ].join(\" | \");\n }\n\n /**\n * Groups instructions by depth for batch processing.\n *\n * @param instructions - Instructions to group\n * @returns Map of depth to instructions (sorted ascending)\n */\n private groupByDepth(instructions: SyncInstruction[]): Map<number, SyncInstruction[]> {\n const grouped = new Map<number, SyncInstruction[]>();\n\n for (const instruction of instructions) {\n const depth = instruction.depth;\n if (!grouped.has(depth)) {\n grouped.set(depth, []);\n }\n grouped.get(depth)!.push(instruction);\n }\n\n // Sort by depth (ascending) for sequential execution\n return new Map([...grouped.entries()].sort((a, b) => a[0] - b[0]));\n }\n\n /**\n * Checks if sync should proceed based on watched fields.\n *\n * @param config - Sync configuration\n * @param changedFields - Fields that changed\n * @returns True if sync should proceed\n */\n private shouldSync(config: SyncConfig, changedFields: string[]): boolean {\n if (config.watchFields.length === 0) {\n return true; // Watch all fields\n }\n\n return config.watchFields.some((field) => changedFields.includes(field));\n }\n\n /**\n * Gets embedded data from the source model.\n *\n * @param data - Source model data or Model instance\n * @param config - Sync configuration\n * @returns Embedded data\n */\n private async getEmbedData(\n data: Record<string, unknown> | Model,\n config: SyncConfig,\n ): Promise<Record<string, unknown>> {\n // If data is a Model instance, call the embed method on it\n if (data instanceof Model) {\n const hidden = data.self().hidden ?? [];\n const withoutHidden = (embedded: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(embedded).filter(([key]) => !hidden.includes(key)));\n\n if (Array.isArray(config.embedKey)) {\n return withoutHidden(data.only(config.embedKey));\n }\n\n if (typeof data[config.embedKey as keyof Model] !== \"function\") {\n return withoutHidden(data[config.embedKey as keyof Model] as Record<string, unknown>);\n }\n\n // The model declares an explicit `embed` column list\n if (data.self().embed) {\n return withoutHidden(data.embedData);\n }\n\n // Default: the model's public (serialised) shape, which strips hidden fields\n return data.toJSON() as Record<string, unknown>;\n }\n\n // Otherwise, return the data as-is\n return data as Record<string, unknown>;\n }\n\n /**\n * Gets sync configurations from the source model.\n *\n * @returns Array of sync configurations\n */\n private getSyncConfigs(): SyncConfig[] {\n const syncWith = (this.sourceModel as any).syncWith;\n\n if (!syncWith || !Array.isArray(syncWith)) {\n return [];\n }\n\n return syncWith.map((builder: any) =>\n typeof builder.build === \"function\" ? builder.build() : builder,\n );\n }\n\n /**\n * Emits a syncing event.\n *\n * @param instruction - The sync instruction\n */\n private async emitSyncingEvent(instruction: SyncInstruction): Promise<void> {\n const payload: SyncEventPayload = {\n sourceModel: instruction.sourceModel,\n sourceId: instruction.sourceId,\n targetModel: instruction.targetModel,\n filter: instruction.filter,\n update: instruction.update,\n affectedCount: 0, // Not yet known\n depth: instruction.depth,\n chain: instruction.chain,\n };\n\n // Emit on source model\n if (typeof (this.sourceModel as any).emitSyncEvent === \"function\") {\n await (this.sourceModel as any).emitSyncEvent(\"syncing\", payload);\n }\n }\n\n /**\n * Emits a synced event.\n *\n * @param context - The sync context\n */\n private async emitSyncedEvent(context: any): Promise<void> {\n const payload: SyncEventPayload = {\n sourceModel: context.sourceModel,\n sourceId: context.sourceId,\n targetModel: context.targetModel,\n filter: context.filter,\n update: context.update,\n affectedCount: context.affectedCount,\n depth: context.currentDepth,\n chain: context.syncChain,\n };\n\n // Emit on source model\n if (typeof (this.sourceModel as any).emitSyncEvent === \"function\") {\n await (this.sourceModel as any).emitSyncEvent(\"synced\", payload);\n }\n }\n\n /**\n * Creates an empty sync result.\n *\n * @returns Empty sync result\n */\n private createEmptyResult(): SyncResult {\n return {\n success: true,\n attempted: 0,\n succeeded: 0,\n failed: 0,\n errors: [],\n depthReached: 0,\n contexts: [],\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync-manager.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/sync/sync-manager.ts"],"sourcesContent":["/**\n * Sync manager service for handling multi-level sync operations.\n *\n * @module cascade-next/sync/sync-manager\n */\n\nimport type { DriverContract } from \"../contracts/database-driver.contract\";\nimport { type ChildModel, Model } from \"../model/model\";\nimport { DEFAULT_MAX_SYNC_DEPTH, SyncContextManager } from \"./sync-context\";\nimport type {\n CollectInstructionsPayload,\n SyncConfig,\n SyncEventPayload,\n SyncInstruction,\n SyncInstructionOptions,\n SyncResult,\n} from \"./types\";\n\n/**\n * Manages sync operations across models with multi-level support.\n */\nexport class SyncManager {\n private readonly sourceModel: ChildModel<Model>;\n private readonly driver: DriverContract;\n\n /**\n * Creates a new sync manager.\n *\n * @param sourceModel - The source model class\n * @param driver - The database driver\n */\n public constructor(sourceModel: ChildModel<Model>, driver: DriverContract) {\n this.sourceModel = sourceModel;\n this.driver = driver;\n }\n\n /**\n * Executes sync operations for a model update.\n *\n * @param sourceId - The source model ID\n * @param updatedData - The updated data to sync (Model instance or plain data)\n * @param changedFields - Fields that were changed (for filtering)\n * @returns Sync result with success status and details\n */\n public async syncUpdate(\n sourceId: string | number,\n updatedData: Record<string, unknown> | Model,\n changedFields: string[],\n ): Promise<SyncResult> {\n try {\n const syncConfigs = this.getSyncConfigs();\n\n if (syncConfigs.length === 0) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: DEFAULT_MAX_SYNC_DEPTH,\n preventCircular: true,\n };\n\n const instructions = await this.collectInstructions({\n sourceId,\n updatedData,\n changedFields,\n syncConfigs,\n options,\n });\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n // Catch-all error handler for unexpected failures\n console.error(`Sync update failed for ${this.sourceModel.name}#${sourceId}:`, error);\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync operations for a model update with a specific config.\n * Used by ModelSyncOperation for event-based sync.\n *\n * @param sourceId - The source model ID\n * @param updatedData - The updated data to sync\n * @param changedFields - Fields that were changed\n * @param config - The sync configuration to use\n * @returns Sync result with success status and details\n */\n public async syncUpdateWithConfig(\n sourceId: string | number,\n updatedData: Record<string, unknown> | Model,\n changedFields: string[],\n config: SyncConfig,\n ): Promise<SyncResult> {\n try {\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: config.maxSyncDepth,\n preventCircular: config.preventCircularSync,\n };\n\n const instructions = await this.collectInstructions({\n sourceId,\n updatedData,\n changedFields,\n syncConfigs: [config],\n options,\n });\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n console.error(\n `Sync update with config failed for ${this.sourceModel.name}#${sourceId}:`,\n error,\n );\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync delete operations with a specific config.\n * Used by ModelSyncOperation for event-based sync.\n *\n * @param sourceId - The source model ID\n * @param config - The sync configuration to use\n * @returns Sync result with success status and details\n */\n public async syncDeleteWithConfig(\n sourceId: string | number,\n config: SyncConfig,\n ): Promise<SyncResult> {\n try {\n if (!config.unsetOnDelete) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: config.maxSyncDepth,\n preventCircular: config.preventCircularSync,\n };\n\n const instructions = await this.collectDeleteInstructions(sourceId, [config], options);\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n console.error(\n `Sync delete with config failed for ${this.sourceModel.name}#${sourceId}:`,\n error,\n );\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Executes sync operations for a model deletion.\n *\n * @param sourceId - The source model ID\n * @returns Sync result with success status and details\n */\n public async syncDelete(sourceId: string | number): Promise<SyncResult> {\n try {\n const syncConfigs = this.getSyncConfigs();\n\n if (syncConfigs.length === 0) {\n return this.createEmptyResult();\n }\n\n const options: SyncInstructionOptions = {\n currentDepth: 1,\n syncChain: [this.sourceModel.name],\n maxDepth: DEFAULT_MAX_SYNC_DEPTH,\n preventCircular: true,\n };\n\n const instructions = await this.collectDeleteInstructions(sourceId, syncConfigs, options);\n\n return await this.executeInstructions(instructions);\n } catch (error) {\n // Catch-all error handler for unexpected failures\n console.error(`Sync delete failed for ${this.sourceModel.name}#${sourceId}:`, error);\n\n return {\n success: false,\n attempted: 0,\n succeeded: 0,\n failed: 1,\n errors: [\n {\n instruction: {\n targetTable: \"\",\n targetModel: \"\",\n filter: {},\n update: {},\n depth: 0,\n chain: [this.sourceModel.name],\n sourceModel: this.sourceModel.name,\n sourceId,\n },\n error: error instanceof Error ? error : new Error(String(error)),\n },\n ],\n depthReached: 0,\n contexts: [],\n };\n }\n }\n\n /**\n * Collects sync instructions recursively with depth limiting.\n *\n * @param payload - Data payload\n * @returns Array of sync instructions\n */\n private async collectInstructions(\n payload: CollectInstructionsPayload,\n ): Promise<SyncInstruction[]> {\n const { sourceId, updatedData, changedFields, syncConfigs, options } = payload;\n const instructions: SyncInstruction[] = [];\n\n for (const config of syncConfigs) {\n // Check if we should sync based on watched fields\n if (!this.shouldSync(config, changedFields)) {\n continue;\n }\n\n // Validate depth and circular references\n const validation = SyncContextManager.validate(\n options.currentDepth,\n options.syncChain,\n config.targetModelClass.name,\n config.maxSyncDepth,\n config.preventCircularSync,\n );\n\n if (!validation.valid) {\n console.warn(`Sync validation failed: ${validation.error}`);\n continue;\n }\n\n // Get embedded data (handle both Model instance and plain data)\n const embedData = await this.getEmbedData(updatedData, config);\n\n // Build instruction\n const instruction = this.buildUpdateInstruction(sourceId, config, embedData, options);\n\n instructions.push(instruction);\n\n // Emit syncing event\n await this.emitSyncingEvent(instruction);\n }\n\n return instructions;\n }\n\n /**\n * Collects delete sync instructions.\n *\n * @param sourceId - Source model ID\n * @param syncConfigs - Sync configurations\n * @param options - Instruction options\n * @returns Array of sync instructions\n */\n private async collectDeleteInstructions(\n sourceId: string | number,\n syncConfigs: SyncConfig[],\n options: SyncInstructionOptions,\n ): Promise<SyncInstruction[]> {\n const instructions: SyncInstruction[] = [];\n\n for (const config of syncConfigs) {\n if (!config.unsetOnDelete) {\n continue;\n }\n\n const validation = SyncContextManager.validate(\n options.currentDepth,\n options.syncChain,\n config.targetModelClass.name,\n config.maxSyncDepth,\n config.preventCircularSync,\n );\n\n if (!validation.valid) {\n continue;\n }\n\n const instruction = this.buildDeleteInstruction(sourceId, config, options);\n instructions.push(instruction);\n\n await this.emitSyncingEvent(instruction);\n }\n\n return instructions;\n }\n\n /**\n * Builds an update sync instruction.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @param embedData - Embedded data to sync\n * @param options - Instruction options\n * @returns Sync instruction\n */\n private buildUpdateInstruction(\n sourceId: string | number,\n config: SyncConfig,\n embedData: Record<string, unknown>,\n options: SyncInstructionOptions,\n ): SyncInstruction {\n const targetModelClass = config.targetModelClass;\n const filter = this.buildFilter(sourceId, config);\n const update = this.buildUpdate(embedData, config);\n\n const instruction: SyncInstruction = {\n targetTable: targetModelClass.table,\n targetModel: targetModelClass.name,\n filter,\n update,\n depth: options.currentDepth,\n chain: [...options.syncChain],\n sourceModel: this.sourceModel.name,\n sourceId,\n };\n\n // Add array update metadata if needed\n if (config.isMany) {\n instruction.isArrayUpdate = true;\n instruction.arrayField = config.targetField;\n instruction.identifierField = config.identifierField;\n instruction.identifierValue = sourceId;\n }\n\n return instruction;\n }\n\n /**\n * Builds a delete sync instruction.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @param options - Instruction options\n * @returns Sync instruction\n */\n private buildDeleteInstruction(\n sourceId: string | number,\n config: SyncConfig,\n options: SyncInstructionOptions,\n ): SyncInstruction {\n const targetModelClass = config.targetModelClass;\n const filter = this.buildFilter(sourceId, config);\n // For arrays, remove only the deleted element instead of wiping the whole field\n const update = config.isMany\n ? { $pull: { [config.targetField]: { [config.identifierField]: sourceId } } }\n : { $unset: { [config.targetField]: 1 } };\n\n return {\n targetTable: targetModelClass.table,\n targetModel: targetModelClass.name,\n filter,\n update,\n depth: options.currentDepth,\n chain: [...options.syncChain],\n sourceModel: this.sourceModel.name,\n sourceId,\n };\n }\n\n /**\n * Builds a filter for identifying target documents.\n *\n * @param sourceId - Source model ID\n * @param config - Sync configuration\n * @returns Filter object\n */\n private buildFilter(sourceId: string | number, config: SyncConfig): Record<string, unknown> {\n if (config.isMany) {\n // For arrays: { \"products.id\": productId }\n return { [`${config.targetField}.${config.identifierField}`]: sourceId };\n } else {\n // For single: { \"category.id\": categoryId }\n return { [`${config.targetField}.${config.identifierField}`]: sourceId };\n }\n }\n\n /**\n * Builds an update operation for syncing data.\n *\n * @param embedData - Embedded data to sync\n * @param config - Sync configuration\n * @returns Update operation object\n */\n private buildUpdate(\n embedData: Record<string, unknown>,\n config: SyncConfig,\n ): Record<string, unknown> {\n if (config.isMany) {\n // For arrays: use positional operator $\n return { $set: { [`${config.targetField}.$`]: embedData } };\n } else {\n // For single: direct set\n return { $set: { [config.targetField]: embedData } };\n }\n }\n\n /**\n * Executes sync instructions, once each, through the driver's dialect-aware\n * sync adapter. Instructions run in depth order and each one records its own\n * outcome, so one failing instruction never re-runs the ones that succeeded.\n *\n * @param instructions - Array of sync instructions\n * @returns Sync result\n */\n private async executeInstructions(instructions: SyncInstruction[]): Promise<SyncResult> {\n const result: SyncResult = {\n success: true,\n attempted: instructions.length,\n succeeded: 0,\n failed: 0,\n errors: [],\n depthReached: 0,\n contexts: [],\n };\n\n if (instructions.length === 0) {\n return result;\n }\n\n const adapter = this.driver.syncAdapter();\n\n for (const [depth, depthInstructions] of this.groupByDepth(instructions)) {\n result.depthReached = Math.max(result.depthReached, depth);\n\n for (const instruction of depthInstructions) {\n try {\n const affected = await adapter.executeBatch([instruction]);\n\n const context = SyncContextManager.createContext(instruction, affected);\n result.contexts.push(context);\n result.succeeded++;\n\n await this.emitSyncedEvent(context);\n } catch (error) {\n result.failed++;\n\n const errorMessage = this.formatSyncError(instruction, error);\n const syncError = new Error(errorMessage);\n\n // Preserve original error stack if available\n if (error instanceof Error && error.stack) {\n syncError.stack = error.stack;\n }\n\n result.errors.push({ instruction, error: syncError });\n\n console.error(`Sync operation failed:`, {\n sourceModel: instruction.sourceModel,\n sourceId: instruction.sourceId,\n targetModel: instruction.targetModel,\n targetTable: instruction.targetTable,\n depth: instruction.depth,\n chain: SyncContextManager.formatChain(instruction.chain),\n filter: instruction.filter,\n error: errorMessage,\n });\n }\n }\n }\n\n result.success = result.failed === 0;\n return result;\n }\n\n /**\n * Formats a sync error with detailed context.\n *\n * @param instruction - The failed instruction\n * @param error - The error that occurred\n * @returns Formatted error message\n */\n private formatSyncError(instruction: SyncInstruction, error: unknown): string {\n const baseMessage = error instanceof Error ? error.message : String(error);\n const chain = SyncContextManager.formatChain(instruction.chain);\n\n return [\n `Sync failed at depth ${instruction.depth}:`,\n `Chain: ${chain} → ${instruction.targetModel}`,\n `Source: ${instruction.sourceModel}#${instruction.sourceId}`,\n `Target: ${instruction.targetTable}`,\n `Error: ${baseMessage}`,\n ].join(\" | \");\n }\n\n /**\n * Groups instructions by depth for batch processing.\n *\n * @param instructions - Instructions to group\n * @returns Map of depth to instructions (sorted ascending)\n */\n private groupByDepth(instructions: SyncInstruction[]): Map<number, SyncInstruction[]> {\n const grouped = new Map<number, SyncInstruction[]>();\n\n for (const instruction of instructions) {\n const depth = instruction.depth;\n if (!grouped.has(depth)) {\n grouped.set(depth, []);\n }\n grouped.get(depth)!.push(instruction);\n }\n\n // Sort by depth (ascending) for sequential execution\n return new Map([...grouped.entries()].sort((a, b) => a[0] - b[0]));\n }\n\n /**\n * Checks if sync should proceed based on watched fields.\n *\n * @param config - Sync configuration\n * @param changedFields - Fields that changed\n * @returns True if sync should proceed\n */\n private shouldSync(config: SyncConfig, changedFields: string[]): boolean {\n if (config.watchFields.length === 0) {\n return true; // Watch all fields\n }\n\n return config.watchFields.some((field) => changedFields.includes(field));\n }\n\n /**\n * Gets embedded data from the source model.\n *\n * @param data - Source model data or Model instance\n * @param config - Sync configuration\n * @returns Embedded data\n */\n private async getEmbedData(\n data: Record<string, unknown> | Model,\n config: SyncConfig,\n ): Promise<Record<string, unknown>> {\n // If data is a Model instance, call the embed method on it\n if (data instanceof Model) {\n const hidden = data.self().hidden ?? [];\n const withoutHidden = (embedded: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(embedded).filter(([key]) => !hidden.includes(key)));\n\n if (Array.isArray(config.embedKey)) {\n return withoutHidden(data.only(config.embedKey));\n }\n\n if (typeof data[config.embedKey as keyof Model] !== \"function\") {\n return withoutHidden(data[config.embedKey as keyof Model] as Record<string, unknown>);\n }\n\n // The model declares an explicit `embed` column list\n if (data.self().embed) {\n return withoutHidden(data.embedData);\n }\n\n // Default: the model's public (serialised) shape, which strips hidden fields\n return data.toJSON() as Record<string, unknown>;\n }\n\n // Otherwise, return the data as-is\n return data as Record<string, unknown>;\n }\n\n /**\n * Gets sync configurations from the source model.\n *\n * @returns Array of sync configurations\n */\n private getSyncConfigs(): SyncConfig[] {\n const syncWith = (this.sourceModel as any).syncWith;\n\n if (!syncWith || !Array.isArray(syncWith)) {\n return [];\n }\n\n return syncWith.map((builder: any) =>\n typeof builder.build === \"function\" ? builder.build() : builder,\n );\n }\n\n /**\n * Emits a syncing event.\n *\n * @param instruction - The sync instruction\n */\n private async emitSyncingEvent(instruction: SyncInstruction): Promise<void> {\n const payload: SyncEventPayload = {\n sourceModel: instruction.sourceModel,\n sourceId: instruction.sourceId,\n targetModel: instruction.targetModel,\n filter: instruction.filter,\n update: instruction.update,\n affectedCount: 0, // Not yet known\n depth: instruction.depth,\n chain: instruction.chain,\n };\n\n // Emit on source model\n if (typeof (this.sourceModel as any).emitSyncEvent === \"function\") {\n await (this.sourceModel as any).emitSyncEvent(\"syncing\", payload);\n }\n }\n\n /**\n * Emits a synced event.\n *\n * @param context - The sync context\n */\n private async emitSyncedEvent(context: any): Promise<void> {\n const payload: SyncEventPayload = {\n sourceModel: context.sourceModel,\n sourceId: context.sourceId,\n targetModel: context.targetModel,\n filter: context.filter,\n update: context.update,\n affectedCount: context.affectedCount,\n depth: context.currentDepth,\n chain: context.syncChain,\n };\n\n // Emit on source model\n if (typeof (this.sourceModel as any).emitSyncEvent === \"function\") {\n await (this.sourceModel as any).emitSyncEvent(\"synced\", payload);\n }\n }\n\n /**\n * Creates an empty sync result.\n *\n * @returns Empty sync result\n */\n private createEmptyResult(): SyncResult {\n return {\n success: true,\n attempted: 0,\n succeeded: 0,\n failed: 0,\n errors: [],\n depthReached: 0,\n contexts: [],\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-writer.utils.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/database-writer.utils.ts"],"mappings":";;;;KAGK,wBAAA;EACH,KAAA,EAAO,KAAK;EACZ,MAAA;EACA,KAAA;EACA,SAAA;EACA,KAAA;AAAA;AAAA,KAGU,sBAAA,
|
|
1
|
+
{"version":3,"file":"database-writer.utils.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/database-writer.utils.ts"],"mappings":";;;;KAGK,wBAAA;EACH,KAAA,EAAO,KAAK;EACZ,MAAA;EACA,KAAA;EACA,SAAA;EACA,KAAA;AAAA;AAAA,KAGU,sBAAA,IACV,OAAA,EAAS,wBAAA,eACI,OAAO;;;;iBAKN,mBAAA,CAAoB,QAAA,EAAU,sBAAA,GAAsB,mBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-writer.utils.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/database-writer.utils.ts"],"sourcesContent":["import { type TransformerCallback } from \"@warlock.js/seal\";\nimport { type Model } from \"../model/model\";\n\ntype transformCallbackOptions = {\n model: Model;\n column: string;\n value: any;\n isChanged: boolean;\n isNew: boolean;\n};\n\nexport type ModelTransformCallback = (options: transformCallbackOptions) => unknown | Promise<unknown>;\n\n/**\n * Transfer value before saving it into the database\n */\nexport function useModelTransformer(callback: ModelTransformCallback) {\n const transformCallback: TransformerCallback = (data, { context }) => {\n const model = context.rootContext?.model as Model | undefined;\n\n // No model (e.g. the schema is reused to validate a request body): nothing to transform\n if (!model) return data;\n\n const column = context.key;\n const value = data;\n const isChanged = model.isDirty(column);\n const isNew = model.isNew;\n return callback({ model, column, value, isChanged, isNew });\n };\n\n return transformCallback;\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"database-writer.utils.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/database-writer.utils.ts"],"sourcesContent":["import { type TransformerCallback } from \"@warlock.js/seal\";\nimport { type Model } from \"../model/model\";\n\ntype transformCallbackOptions = {\n model: Model;\n column: string;\n value: any;\n isChanged: boolean;\n isNew: boolean;\n};\n\nexport type ModelTransformCallback = (\n options: transformCallbackOptions,\n) => unknown | Promise<unknown>;\n\n/**\n * Transfer value before saving it into the database\n */\nexport function useModelTransformer(callback: ModelTransformCallback) {\n const transformCallback: TransformerCallback = (data, { context }) => {\n const model = context.rootContext?.model as Model | undefined;\n\n // No model (e.g. the schema is reused to validate a request body): nothing to transform\n if (!model) return data;\n\n const column = context.key;\n const value = data;\n const isChanged = model.isDirty(column);\n const isNew = model.isNew;\n return callback({ model, column, value, isChanged, isNew });\n };\n\n return transformCallback;\n}\n"],"mappings":";;;;AAkBA,SAAgB,oBAAoB,UAAkC;CACpE,MAAM,qBAA0C,MAAM,EAAE,cAAc;EACpE,MAAM,QAAQ,QAAQ,aAAa;EAGnC,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,SAAS,QAAQ;EACvB,MAAM,QAAQ;EACd,MAAM,YAAY,MAAM,QAAQ,MAAM;EACtC,MAAM,QAAQ,MAAM;EACpB,OAAO,SAAS;GAAE;GAAO;GAAQ;GAAO;GAAW;EAAM,CAAC;CAC5D;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-valid-date-value.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/is-valid-date-value.ts"],"sourcesContent":["// Strings must carry a time part AND a timezone (Z or ±hh:mm), so plain text such as\n// \"2024-01-15\" or a zone-less local datetime is never turned into a Date.\nconst isoRegex = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}:?\\d{2})$/;\n\n/**\n * Check if the given value is a valid date value\n */\nexport function isValidDateValue(value: unknown): boolean {\n // ✅ Handle timestamps\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return false;\n\n const date = new Date(value);\n return !Number.isNaN(date.getTime());\n }\n\n // ❌ Only allow strict ISO strings\n if (typeof value !== \"string\") return false;\n\n if (!isoRegex.test(value)) return false;\n\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return false;\n\n // 🔥 Critical step: prevent JS auto-correction\n // Example: \"2023-02-31\" → March 3 (WRONG but \"valid\")\n // Checked on the written date part itself, so a timezone offset can't shift it.\n const datePart = value.split(\"T\")[0];\n if (datePart === undefined) return false;\n\n const [y, m, d] = datePart.split(\"-\").map(Number);\n if (y === undefined || m === undefined || d === undefined) return false;\n\n const written = new Date(Date.UTC(y, m - 1, d));\n\n return written.getUTCFullYear() === y && written.getUTCMonth() + 1 === m && written.getUTCDate() === d;\n}\n"],"mappings":";AAEA,MAAM,WAAW;;;;AAKjB,SAAgB,iBAAiB,OAAyB;CAExD,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO;EAEpC,MAAM,OAAO,IAAI,KAAK,KAAK;EAC3B,OAAO,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC;CACrC;CAGA,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,IAAI,CAAC,SAAS,KAAK,KAAK,GAAG,OAAO;CAElC,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG,OAAO;CAKzC,MAAM,WAAW,MAAM,MAAM,GAAG,CAAC,CAAC;CAClC,IAAI,aAAa,QAAW,OAAO;CAEnC,MAAM,CAAC,GAAG,GAAG,KAAK,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CAChD,IAAI,MAAM,UAAa,MAAM,UAAa,MAAM,QAAW,OAAO;CAElE,MAAM,UAAU,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;CAE9C,
|
|
1
|
+
{"version":3,"file":"is-valid-date-value.mjs","names":[],"sources":["../../../../../../../../packages/cascade/src/utils/is-valid-date-value.ts"],"sourcesContent":["// Strings must carry a time part AND a timezone (Z or ±hh:mm), so plain text such as\n// \"2024-01-15\" or a zone-less local datetime is never turned into a Date.\nconst isoRegex = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}:?\\d{2})$/;\n\n/**\n * Check if the given value is a valid date value\n */\nexport function isValidDateValue(value: unknown): boolean {\n // ✅ Handle timestamps\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return false;\n\n const date = new Date(value);\n return !Number.isNaN(date.getTime());\n }\n\n // ❌ Only allow strict ISO strings\n if (typeof value !== \"string\") return false;\n\n if (!isoRegex.test(value)) return false;\n\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return false;\n\n // 🔥 Critical step: prevent JS auto-correction\n // Example: \"2023-02-31\" → March 3 (WRONG but \"valid\")\n // Checked on the written date part itself, so a timezone offset can't shift it.\n const datePart = value.split(\"T\")[0];\n if (datePart === undefined) return false;\n\n const [y, m, d] = datePart.split(\"-\").map(Number);\n if (y === undefined || m === undefined || d === undefined) return false;\n\n const written = new Date(Date.UTC(y, m - 1, d));\n\n return (\n written.getUTCFullYear() === y && written.getUTCMonth() + 1 === m && written.getUTCDate() === d\n );\n}\n"],"mappings":";AAEA,MAAM,WAAW;;;;AAKjB,SAAgB,iBAAiB,OAAyB;CAExD,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO;EAEpC,MAAM,OAAO,IAAI,KAAK,KAAK;EAC3B,OAAO,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC;CACrC;CAGA,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,IAAI,CAAC,SAAS,KAAK,KAAK,GAAG,OAAO;CAElC,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG,OAAO;CAKzC,MAAM,WAAW,MAAM,MAAM,GAAG,CAAC,CAAC;CAClC,IAAI,aAAa,QAAW,OAAO;CAEnC,MAAM,CAAC,GAAG,GAAG,KAAK,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CAChD,IAAI,MAAM,UAAa,MAAM,UAAa,MAAM,QAAW,OAAO;CAElE,MAAM,UAAU,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;CAE9C,OACE,QAAQ,eAAe,MAAM,KAAK,QAAQ,YAAY,IAAI,MAAM,KAAK,QAAQ,WAAW,MAAM;AAElG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-writer.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/writer/database-writer.ts"],"mappings":";;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,cAAA;
|
|
1
|
+
{"version":3,"file":"database-writer.d.mts","names":[],"sources":["../../../../../../../../packages/cascade/src/writer/database-writer.ts"],"mappings":";;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,cAAA;EA6HvB;EAAA,iBA3HG,KAAA;EAoPgB;EAAA,iBAjPhB,kBAAA;EAgSJ;EAAA,iBA7RI,IAAA;EA6Ra;EAAA,iBA1Rb,UAAA;EA0RmD;EAAA,iBAvRnD,MAAA;EAmXT;EAAA,iBAhXS,KAAA;EA+aJ;EAAA,iBA5aI,UAAA;EA+cT;EAAA,iBA5cS,MAAA;EA+hBE;EAAA,iBA5hBF,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAkBb,IAAA,CAAK,OAAA,GAAS,aAAA,GAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;;UAmE1C,eAAA;;;;;;;EAyHD,gBAAA,IAAoB,OAAA;;;;;;;;UAYnB,aAAA;;;;;;;;;;;;EAmCD,gBAAA,CAAiB,OAAA,GAAS,aAAA,GAAqB,OAAA,CAAQ,MAAA;;;;;;;;UA4CtD,aAAA;;;;;;UAgDN,kBAAA;;;;;;;;;;;;UAsDA,qBAAA;;;;;;EASK,cAAA,IAAkB,OAAA;;;;;;;;;;;;;;;;;;;;;;UAmCvB,qBAAA;;;;;;;;;;UAmFA,WAAA;AAAA"}
|