// SPDX-License-Identifier: AGPL-3.0-or-later import {upsertOne} from './CassandraQueryExecution'; import type {ColumnName, DbOp, PatchObject, RowValue, Table} from './CassandraTypes'; import {Db, nextVersion} from './CassandraTypes'; export async function executeVersionedUpdate< Row extends { version?: number | null; }, PK extends ColumnName, Patch extends PatchObject = PatchObject, >( fetchCurrent: () => Promise, buildPatch: (current: Row | null) => { pk: Record; patch: Patch; }, table: Table, opts?: { initialData?: Row | null; }, ): Promise<{ finalVersion: number | null; previousData: Row | null; }> { const current = opts?.initialData !== undefined ? opts.initialData : await fetchCurrent(); const currentVersion = current?.version ?? null; const newVersion = nextVersion(currentVersion); const {pk, patch} = buildPatch(current); await upsertOne( table.patchByPk( pk as Pick, {...patch, version: Db.set(newVersion)} as Partial<{ [K in Exclude, PK>]: DbOp>; }>, ), ); return {finalVersion: newVersion, previousData: current}; } export function applyPatchToRow(current: Partial | null, patch: PatchObject): Partial { const next = {...(current ?? {})} as Record; for (const [column, op] of Object.entries(patch)) { if (op.kind === 'clear') { next[column] = null; continue; } next[column] = op.value; } return next as Partial; } function valuesEqual(a: unknown, b: unknown): boolean { if (a === b) return true; if (a == null || b == null) return false; if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (a instanceof Set && b instanceof Set) { if (a.size !== b.size) return false; for (const item of a) { if (!b.has(item)) return false; } return true; } if (a instanceof Map && b instanceof Map) { if (a.size !== b.size) return false; for (const [key, val] of a) { if (!b.has(key) || !valuesEqual(val, b.get(key))) return false; } return true; } if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) { return a.equals(b); } if (typeof a === 'object' && typeof b === 'object') { const keysA = Object.keys(a as Record); const keysB = Object.keys(b as Record); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; if (!valuesEqual((a as Record)[key], (b as Record)[key])) { return false; } } return true; } return a === b; } export function buildPatchFromData( newData: Partial, oldData: Partial | null, columns: ReadonlyArray, pkColumns: ReadonlyArray, ): Record> { const patch: Record> = {}; for (const col of columns) { if (pkColumns.includes(col)) continue; if (col === 'version') continue; const colName = col as string; const newVal = (newData as Record)[colName]; if (newVal === undefined) continue; const oldVal = oldData ? (oldData as Record)[colName] : undefined; if (valuesEqual(newVal, oldVal)) continue; if (newVal === null) { if (oldData !== null && oldVal !== null && oldVal !== undefined) { patch[colName] = Db.clear(); } } else { patch[colName] = Db.set(newVal); } } return patch; }