import type { AnyObject, FieldFilter, InsertParams, UpdateParams } from "prostgles-types"; import { asName } from "prostgles-types"; import type { InsertRule, UpdateRule } from "../../PublishParser/PublishParser"; import type { LocalParams } from "../DboBuilder"; import { getClientErrorFromPGError, withUserRLS } from "../DboBuilder"; import type { TableHandler } from "./TableHandler"; import { getSelectItemQuery } from "./TableHandler"; type RunInsertUpdateQueryArgs = { tableHandler: TableHandler; queryWithoutUserRLS: string; localParams: LocalParams | undefined; fields: FieldFilter | undefined; returningFields: FieldFilter | undefined; } & ( | { command: "insert"; params: InsertParams | undefined; rule: InsertRule | undefined; data: AnyObject | AnyObject[]; isMultiInsert: boolean; nestedInsertsResultsObj?: undefined; } | { command: "update"; nestedInsertsResultsObj: Record; params: UpdateParams | undefined; rule: UpdateRule | undefined; data: Record; } ); export const runInsertUpdateQuery = async (args: RunInsertUpdateQueryArgs) => { const { tableHandler, queryWithoutUserRLS, rule, localParams, fields, returningFields, params, nestedInsertsResultsObj, data, command, } = args; const { name } = tableHandler; const returningSelectItems = await tableHandler.prepareReturning( params?.returning, tableHandler.parseFieldFilter(returningFields), ); const { checkFilter } = rule ?? {}; let checkCondition = "WHERE FALSE"; if (checkFilter) { const checkCond = await tableHandler.prepareWhere({ select: undefined, localParams: undefined, tableRule: undefined, filter: checkFilter, addWhere: false, }); checkCondition = `WHERE NOT (${checkCond.where})`; } const hasReturning = !!returningSelectItems.length; const userRLS = withUserRLS(localParams, ""); const escapedTableName = asName(name); const query = ` ${userRLS} WITH ${escapedTableName} AS ( ${queryWithoutUserRLS} RETURNING * ) SELECT count(*) as row_count, ( SELECT json_agg(item) FROM ( SELECT * FROM ${escapedTableName} ) item ) as modified, ( SELECT json_agg(item) FROM ( SELECT ${!hasReturning ? "1" : getSelectItemQuery(returningSelectItems)} FROM ${escapedTableName} WHERE ${hasReturning ? "TRUE" : "FALSE"} ) item ) as modified_returning, ( SELECT json_agg(item) FROM ( SELECT * FROM ${escapedTableName} ${checkCondition} LIMIT 5 ) item ) AS failed_check FROM ${escapedTableName} `; const allowedFieldKeys = tableHandler.parseFieldFilter(fields); let result: { row_count: number | null; modified: AnyObject[] | null; failed_check: AnyObject[] | null; modified_returning: AnyObject[] | null; }; const queryType = "one"; const tx = localParams?.tx?.t || tableHandler.tx?.t; if (tx) { result = await tx[queryType](query).catch((err: unknown) => getClientErrorFromPGError(err, { type: "tableMethod", localParams, view: tableHandler, allowedKeys: allowedFieldKeys, prostgles: tableHandler.dboBuilder.prostgles, }), ); } else { result = await tableHandler.db .tx((t) => (t as any)[queryType](query)) .catch((err) => getClientErrorFromPGError(err, { type: "tableMethod", localParams, view: tableHandler, allowedKeys: allowedFieldKeys, prostgles: tableHandler.dboBuilder.prostgles, }), ); } if (checkFilter && result.failed_check?.length) { throw new Error( `Insert ${name} records failed the check condition: ${JSON.stringify(checkFilter, null, 2)}`, ); } const rows = result.modified ?? []; const finalDBtx = tableHandler.getFinalDBtx(localParams); const { postValidate } = rule ?? {}; const hooks = tableHandler.getHooksAndChecks( command === "insert" ? { name: command, rule } : { name: command, rule }, ); let changedFieldsSet = undefined as undefined | Set; const getChangedFieldsSet = () => { changedFieldsSet ??= new Set( (isArray(data) ? data : [data]).map((row) => Object.keys(row)).flat(), ); return changedFieldsSet; }; const applicableHooks = hooks.filter((hook) => { if (hook.type === "checkFilter") return; if (hook.type === "postValidate") return true; const { commands, changedFields } = hook; return ( commands[command] && (!changedFields || changedFields.some((f) => getChangedFieldsSet().has(f))) ); }); if (postValidate && !localParams) { throw new Error("Unexpected: no localParams for postValidate"); } if (applicableHooks.length) { if (!finalDBtx) throw new Error("Unexpected: no dbTX for hooks/postValidate"); for (const row of rows) { const commonParams = { row: row, tx: tx || tableHandler.db, dbx: finalDBtx, command, data, } as const; for (const hook of applicableHooks) { if (hook.type === "afterEach") { await hook.validate({ ...commonParams, localParams, }); } else if (hook.type === "postValidate") { if (!localParams) throw new Error("Unexpected: no localParams for postValidate"); await hook.validate({ ...commonParams, localParams, }); } } } for (const hook of applicableHooks) { if (hook.type === "afterAll") { await hook.validate({ tx: tx || tableHandler.db, dbx: finalDBtx, command, data: Array.isArray(data) ? data : [data], rows, localParams, }); } } } let returnMany = false; if (args.command === "update") { const { multi = true } = args.params || {}; if (!multi && result.row_count && +result.row_count > 1) { throw `More than 1 row modified: ${result.row_count} rows affected`; } if (hasReturning) { returnMany = multi; } } else { returnMany = args.isMultiInsert; } if (!hasReturning) return undefined; const modified_returning = result.modified_returning?.map((d) => ({ ...d, ...nestedInsertsResultsObj, })); return returnMany ? modified_returning : modified_returning?.[0]; }; const isArray = (data: T): data is Extract => Array.isArray(data);