class InsertStatement { order; #keys; #data; constructor(keys, data, order) { this.order = order; this.#keys = keys; this.#data = data; } getHash() { return JSON.stringify(this.#data); } getData() { const data = {}; this.#keys.forEach((key, idx) => (data[key] = this.#data[idx])); return data; } } class DeleteStatement { #keys; #cond; constructor(keys, cond) { this.#keys = keys; this.#cond = cond; } getHash() { return JSON.stringify(this.#cond); } getCondition() { const cond = {}; this.#keys.forEach((key, idx) => (cond[key] = this.#cond[idx])); return cond; } } export class PivotCollectionPersister { #inserts = new Map(); #upserts = new Map(); #deletes = new Map(); #batchSize; #order = 0; #meta; #driver; #ctx; #schema; #loggerContext; constructor(meta, driver, ctx, schema, loggerContext) { this.#meta = meta; this.#driver = driver; this.#ctx = ctx; this.#schema = schema; this.#loggerContext = loggerContext; this.#batchSize = this.#driver.config.get('batchSize'); } enqueueUpdate(prop, insertDiff, deleteDiff, pks, isInitialized = true) { if (insertDiff.length) { if (isInitialized) { this.enqueueInsert(prop, insertDiff, pks); } else { this.enqueueUpsert(prop, insertDiff, pks); } } if (deleteDiff === true || (Array.isArray(deleteDiff) && deleteDiff.length)) { this.enqueueDelete(prop, deleteDiff, pks); } } enqueueInsert(prop, insertDiff, pks) { for (const fks of insertDiff) { const statement = this.createInsertStatement(prop, fks, pks); const hash = statement.getHash(); if (prop.owner || !this.#inserts.has(hash)) { this.#inserts.set(hash, statement); } } } enqueueUpsert(prop, insertDiff, pks) { for (const fks of insertDiff) { const statement = this.createInsertStatement(prop, fks, pks); const hash = statement.getHash(); if (prop.owner || !this.#upserts.has(hash)) { this.#upserts.set(hash, statement); } } } createInsertStatement(prop, fks, pks) { const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks); return new InsertStatement(keys, data, this.#order++); } enqueueDelete(prop, deleteDiff, pks) { if (deleteDiff === true) { const { data, keys } = this.buildPivotKeysAndData(prop, [], pks, true); const statement = new DeleteStatement(keys, data); this.#deletes.set(statement.getHash(), statement); return; } for (const fks of deleteDiff) { const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks); const statement = new DeleteStatement(keys, data); this.#deletes.set(statement.getHash(), statement); } } /** * Build the keys and data arrays for pivot table operations. * Handles polymorphic M:N by prepending the discriminator column/value. */ buildPivotKeysAndData(prop, fks, pks, deleteAll = false) { let data; let keys; if (deleteAll) { data = pks; keys = prop.joinColumns; } else { data = prop.owner ? [...fks, ...pks] : [...pks, ...fks]; keys = prop.owner ? [...prop.inverseJoinColumns, ...prop.joinColumns] : [...prop.joinColumns, ...prop.inverseJoinColumns]; } if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) { data = [prop.discriminatorValue, ...data]; keys = [prop.discriminatorColumn, ...keys]; } return { data, keys }; } collectStatements(statements) { const items = []; for (const statement of statements.values()) { items[statement.order] = statement.getData(); } return items.filter(Boolean); } async execute() { if (this.#deletes.size > 0) { const deletes = [...this.#deletes.values()]; for (let i = 0; i < deletes.length; i += this.#batchSize) { const chunk = deletes.slice(i, i + this.#batchSize); const cond = { $or: [] }; for (const item of chunk) { cond.$or.push(item.getCondition()); } await this.#driver.nativeDelete(this.#meta.class, cond, { ctx: this.#ctx, schema: this.#schema, loggerContext: this.#loggerContext, }); } } if (this.#inserts.size > 0) { const filtered = this.collectStatements(this.#inserts); for (let i = 0; i < filtered.length; i += this.#batchSize) { const chunk = filtered.slice(i, i + this.#batchSize); await this.#driver.nativeInsertMany(this.#meta.class, chunk, { ctx: this.#ctx, schema: this.#schema, convertCustomTypes: false, processCollections: false, loggerContext: this.#loggerContext, }); } } if (this.#upserts.size > 0) { const filtered = this.collectStatements(this.#upserts); for (let i = 0; i < filtered.length; i += this.#batchSize) { const chunk = filtered.slice(i, i + this.#batchSize); await this.#driver.nativeUpdateMany(this.#meta.class, [], chunk, { ctx: this.#ctx, schema: this.#schema, convertCustomTypes: false, processCollections: false, upsert: true, onConflictAction: 'ignore', loggerContext: this.#loggerContext, }); } } } }