import { C6Constants as C6C } from "../constants/C6Constants"; import { C6RestfulModel, iRestMethods, RequestQueryBody } from "../types/ormInterfaces"; const isPlainObject = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const assertOrderTerms = (value: unknown, path: string): void => { if (value === undefined) return; if (!Array.isArray(value)) { throw new Error(`${path} expects an array of terms using [expression, direction?] syntax. Legacy ORDER object maps are not supported.`); } }; export function normalizeRequestOrder< Method extends iRestMethods, T extends Record, Custom extends Record = {}, Overrides extends Record = {} > ( request: RequestQueryBody, ): RequestQueryBody { if (request == null || typeof request !== 'object') return request; const requestObj = request as Record; const rootOrder = requestObj[C6C.ORDER]; const paginationRaw = requestObj[C6C.PAGINATION]; const paginationOrder = isPlainObject(paginationRaw) ? paginationRaw[C6C.ORDER] : undefined; assertOrderTerms(rootOrder, C6C.ORDER); assertOrderTerms(paginationOrder, `${C6C.PAGINATION}.${C6C.ORDER}`); if (rootOrder === undefined) { return request; } if (paginationOrder !== undefined) { throw new Error(`Specify ${C6C.ORDER} in only one place. Use either ${C6C.ORDER} or ${C6C.PAGINATION}.${C6C.ORDER}, not both.`); } if (paginationRaw != null && !isPlainObject(paginationRaw)) { throw new Error(`${C6C.PAGINATION} must be an object when ${C6C.ORDER} is provided.`); } const { [C6C.ORDER]: _removedOrder, ...rest } = requestObj; return { ...(rest as any), [C6C.PAGINATION]: { ...(isPlainObject(paginationRaw) ? paginationRaw : {}), [C6C.ORDER]: rootOrder, }, } as RequestQueryBody; } /** * Converts a singular T-shaped request into complex ORM format for GET/PUT/DELETE * Enforces that all primary keys are present for singular syntax and that the table has PKs. * Optionally accepts a previously removed primary key (key/value) to reconstruct WHERE. */ export function normalizeSingularRequest< Method extends iRestMethods, T extends Record, Custom extends Record = {}, Overrides extends Record = {} > ( requestMethod: Method, request: RequestQueryBody, restModel: C6RestfulModel, removedPrimary?: { key: string; value: any } ): RequestQueryBody { if (request == null || typeof request !== 'object') return request; const complexShapeKeys: Set = new Set([ C6C.SELECT, C6C.UPDATE, C6C.DELETE, C6C.WHERE, C6C.JOIN, C6C.GROUP_BY, C6C.HAVING, C6C.INDEX_HINTS, ]); const specialKeys: Set = new Set([ C6C.DB, C6C.SELECT, C6C.UPDATE, C6C.DELETE, C6C.WHERE, C6C.JOIN, C6C.ORDER, C6C.GROUP_BY, C6C.HAVING, C6C.INDEX_HINTS, C6C.PAGINATION, ]); // Determine if the request is already complex (has any special key besides PAGINATION) const keys = Object.keys(request as any); const hasComplexKeys = keys.some(k => complexShapeKeys.has(k)); if (hasComplexKeys) return request; // already complex // We treat it as singular when it's not complex. // For GET, PUT, DELETE only if (!(requestMethod === C6C.GET || requestMethod === C6C.PUT || requestMethod === C6C.DELETE)) { return request; } const pkShorts: string[] = Array.isArray(restModel.PRIMARY_SHORT) ? [...restModel.PRIMARY_SHORT] : []; const pkFulls: string[] = Array.isArray((restModel as any).PRIMARY) ? [...(restModel as any).PRIMARY] : []; const resolveShortKey = (key: string): string => { const cols = (restModel as any).COLUMNS || {}; return (cols as any)[key] ?? key; }; if (!pkShorts.length) { // For GET requests, do not enforce primary key presence; treat as a collection query. if (requestMethod === C6C.GET) return request; throw new Error(`Table (${restModel.TABLE_NAME}) has no primary key; singular request syntax is not allowed.`); } // Build pk map from request + possibly removed primary key (accept short or fully-qualified keys) const pkValues: Record = {}; const requestObj = request as any; for (const pkShort of pkShorts) { // 1) direct short key let value = requestObj[pkShort]; if (value === undefined) { // 2) fully-qualified key matching this short key (from PRIMARY list or by concatenation) const fqCandidate = `${restModel.TABLE_NAME}.${pkShort}`; const fqKey = pkFulls.find( fq => fq === fqCandidate || fq.endsWith(`.${pkShort}`) ) ?? fqCandidate; value = requestObj[fqKey]; } if (value === undefined && removedPrimary) { // 3) removedPrimary may provide either short or fully-qualified key const removedKeyShort = resolveShortKey(removedPrimary.key); if (removedKeyShort === pkShort) value = removedPrimary.value; } if (value !== undefined && value !== null) { pkValues[pkShort] = value; } } const missing = pkShorts.filter(pk => !(pk in pkValues)); if (missing.length) { // For GET requests, if not all PKs are provided, treat as a collection query and leave as-is. if (requestMethod === C6C.GET) { return request; } throw new Error(`Singular request requires all primary key(s) [${pkShorts.join(', ')}] for table (${restModel.TABLE_NAME}). Missing: [${missing.join(', ')}]`); } // Strip API metadata that should remain at root const { ORDER, GROUP_BY, HAVING, INDEX_HINTS, dataInsertMultipleRows, cacheResults, skipReactBootstrap, fetchDependencies, debug, success, error, ...rest } = request as any; // Map short primary keys to fully-qualified column names const shortToFull: Record = {}; for (const [full, short] of Object.entries(restModel.COLUMNS || {})) { shortToFull[short as string] = full; } const pkFullValues = Object.fromEntries( Object.entries(pkValues).map(([k, v]) => [shortToFull[k] ?? k, v]) ); const pkWhereExpressions = Object.fromEntries( Object.entries(pkFullValues).map(([column, value]) => [column, [C6C.EQUAL, [C6C.LIT, value]]]), ); if (requestMethod === C6C.GET) { const normalized: any = { WHERE: { ...pkWhereExpressions }, }; // Preserve pagination if any was added previously if ((request as any)[C6C.PAGINATION]) { normalized[C6C.PAGINATION] = (request as any)[C6C.PAGINATION]; } return { ...normalized, ...(ORDER !== undefined ? { [C6C.ORDER]: ORDER } : {}), ...(GROUP_BY !== undefined ? { [C6C.GROUP_BY]: GROUP_BY } : {}), ...(HAVING !== undefined ? { [C6C.HAVING]: HAVING } : {}), ...(INDEX_HINTS !== undefined ? { [C6C.INDEX_HINTS]: INDEX_HINTS } : {}), dataInsertMultipleRows, cacheResults, skipReactBootstrap, fetchDependencies, debug, success, error, } as any; } if (requestMethod === C6C.DELETE) { const normalized: any = { [C6C.DELETE]: true, WHERE: { ...pkWhereExpressions }, }; return { ...normalized, ...(ORDER !== undefined ? { [C6C.ORDER]: ORDER } : {}), ...(GROUP_BY !== undefined ? { [C6C.GROUP_BY]: GROUP_BY } : {}), ...(HAVING !== undefined ? { [C6C.HAVING]: HAVING } : {}), ...(INDEX_HINTS !== undefined ? { [C6C.INDEX_HINTS]: INDEX_HINTS } : {}), dataInsertMultipleRows, cacheResults, skipReactBootstrap, fetchDependencies, debug, success, error, } as any; } // PUT const updateBody: Record = {}; for (const k of Object.keys(rest)) { // Skip special request keys if any slipped through if (specialKeys.has(k)) continue; const shortKey = resolveShortKey(k); if (pkShorts.includes(shortKey)) continue; // don't update PK columns (short or fully qualified) updateBody[shortKey] = (rest as any)[k]; } if (Object.keys(updateBody).length === 0) { throw new Error(`Singular PUT request for table (${restModel.TABLE_NAME}) must include at least one non-primary field to update.`); } const normalized: any = { [C6C.UPDATE]: updateBody, WHERE: { ...pkWhereExpressions }, }; return { ...normalized, ...(ORDER !== undefined ? { [C6C.ORDER]: ORDER } : {}), ...(GROUP_BY !== undefined ? { [C6C.GROUP_BY]: GROUP_BY } : {}), ...(HAVING !== undefined ? { [C6C.HAVING]: HAVING } : {}), ...(INDEX_HINTS !== undefined ? { [C6C.INDEX_HINTS]: INDEX_HINTS } : {}), dataInsertMultipleRows, cacheResults, skipReactBootstrap, fetchDependencies, debug, success, error, } as any; }