UNPKG

orange-orm

Version:

Object Relational Mapper

1,650 lines (1,485 loc) 556 kB
void !function() { typeof self === 'undefined' && typeof global === 'object' ? global.self = global : null; }();import * as fastJsonPatch from 'fast-json-patch'; import * as axios from 'axios'; import * as _default from 'rfdc/default'; import * as ajv from 'ajv'; import * as onChange from '@lroal/on-change'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function getDefaultExportFromNamespaceIfPresent (n) { return n && Object.prototype.hasOwnProperty.call(n, 'default') ? n['default'] : n; } var getTSDefinition_1; var hasRequiredGetTSDefinition; function requireGetTSDefinition () { if (hasRequiredGetTSDefinition) return getTSDefinition_1; hasRequiredGetTSDefinition = 1; const typeMap = { StringColumn: 'string', BooleanColumn: 'boolean', UUIDColumn: 'string', BinaryColumn: 'string', JSONColumn: 'object', DateColumn: 'Date | string', NumberColumn: 'number', }; function getTSDefinition(tableConfigs, {isNamespace = false, isHttp = false} = {}) { const rootTablesAdded = new Map(); const tableNames = new Set(); const tablesAdded = new Map(); let src = ''; const defs = tableConfigs.map(getTSDefinitionTable).join(''); const tables = tableConfigs.reduce((tables, x) => { tables[x.name] = x.table; return tables; }, {}); src += getPrefixTs(isNamespace); if (isNamespace) src += startNamespace(tables, isHttp); src += defs; src += getRdbClientTs(tables, isHttp); if (isNamespace) src += '}'; return src; function getTSDefinitionTable({table, customFilters, name}) { let Name = name.substr(0, 1).toUpperCase() + name.substr(1); name = name.substr(0, 1).toLowerCase() + name.substr(1); let result = '' + getTable(table, Name, name, customFilters); return result; } function getTable(table, Name, name, customFilters) { const _columns = columns(table); const _tableRelations = tableRelations(table); return ` export interface ${Name}Table { count(filter?: RawFilter): Promise<number>; getAll(): Promise<${Name}Array>; getAll(fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; getMany(filter?: RawFilter): Promise<${Name}Array>; getMany(filter: RawFilter, fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; getMany(${name}s: Array<${Name}>): Promise<${Name}Array>; getMany(${name}s: Array<${Name}>, fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; getOne(filter?: RawFilter): Promise<${Name}Row>; getOne(filter?: RawFilter, fetchingStrategy?: ${Name}Strategy): Promise<${Name}Row>; getOne(${name}: ${Name}): Promise<${Name}Row>; getOne(${name}: ${Name}, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row>; getById(${getIdArgs(table)}): Promise<${Name}Row>; getById(${getIdArgs(table)}, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row>; replace(${name}s: ${Name}[] | ${Name}): Promise<void>; replace(${name}s: ${Name}[], fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; replace(${name}: ${Name}, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row>; update(${name}: ${Name}): Promise<void>; update(${name}: ${Name}, whereStrategy: ${Name}Strategy): Promise<void>; update(${name}: ${Name}, whereStrategy: ${Name}Strategy): Promise<void>; update(${name}: ${Name}, whereStrategy: ${Name}Strategy, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row[]>; updateChanges(${name}s: ${Name}[], old${name}s: ${Name}[]): Promise<${Name}Array>; updateChanges(${name}s: ${Name}[],old${name}s: ${Name}[], fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; updateChanges(${name}: ${Name}, old${name}: ${Name}): Promise<${Name}Row>; updateChanges(${name}: ${Name},old${name}: ${Name}, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row>; insert(${name}s: ${Name}[]): Promise<${Name}Array>; insert(${name}s: ${Name}[], fetchingStrategy: ${Name}Strategy): Promise<${Name}Array>; insert(${name}: ${Name}): Promise<${Name}Row>; insert(${name}: ${Name}, fetchingStrategy: ${Name}Strategy): Promise<${Name}Row>; insertAndForget(${name}s: ${Name}[]): Promise<void>; insertAndForget(${name}: ${Name}): Promise<void>; delete(filter?: RawFilter): Promise<void>; delete(${name}s: Array<${Name}>): Promise<void>; deleteCascade(filter?: RawFilter): Promise<void>; deleteCascade(${name}s: Array<${Name}>): Promise<void>; proxify(${name}s: ${Name}[]): ${Name}Array; proxify(${name}s: ${Name}[], fetchingStrategy: ${Name}Strategy): ${Name}Array; proxify(${name}: ${Name}): ${Name}Row; proxify(${name}: ${Name}, fetchingStrategy: ${Name}Strategy): ${Name}Row; patch(patch: JsonPatch): Promise<void>; patch(patch: JsonPatch, concurrency: ${Name}Concurrency, fetchingStrategy?: ${Name}Strategy): Promise<void>; customFilters: ${Name}CustomFilters; ${_columns} ${_tableRelations} } export interface ${Name}ExpressConfig { baseFilter?: RawFilter | ((context: ExpressContext) => RawFilter | Promise<RawFilter>); customFilters?: Record<string, (context: ExpressContext,...args: any[]) => RawFilter | Promise<RawFilter>>; concurrency?: ${Name}Concurrency; readonly?: boolean; disableBulkDeletes?: boolean; } export interface ${Name}HonoConfig { baseFilter?: RawFilter | ((context: HonoContext) => RawFilter | Promise<RawFilter>); customFilters?: Record<string, (context: HonoContext,...args: any[]) => RawFilter | Promise<RawFilter>>; concurrency?: ${Name}Concurrency; readonly?: boolean; disableBulkDeletes?: boolean; } export interface ${Name}CustomFilters { ${getCustomFilters(customFilters)} } export interface ${Name}Array extends Array<${Name}> { saveChanges(): Promise<void>; saveChanges(concurrency: ${Name}Concurrency, fetchingStrategy?: ${Name}Strategy): Promise<void>; acceptChanges(): void; clearChanges(): void; refresh(): Promise<void>; refresh(fetchingStrategy: ${Name}Strategy): Promise<void>; delete(): Promise<void>; delete(options: ${Name}Concurrency): Promise<void>; } export interface ${Name}Row extends ${Name} { saveChanges(): Promise<void>; saveChanges(concurrency: ${Name}Concurrency, fetchingStrategy?: ${Name}Strategy): Promise<void>; acceptChanges(): void; clearChanges(): void; refresh(): Promise<void>; refresh(fetchingStrategy: ${Name}Strategy): Promise<void>; delete(): Promise<void>; delete(options: ${Name}Concurrency): Promise<void>; } ${Concurrency(table, Name, true)} `; } function getIdArgs(table) { let result = []; for (let i = 0; i < table._primaryColumns.length; i++) { let column = table._primaryColumns[i]; result.push(`${column.alias}: ${typeMap[column.tsType]}`); } return result.join(', '); } function tableRelations(table) { let relations = table._relations; let result = ''; for (let relationName in relations) { const tableName = getTableName(relations[relationName], relationName); result += `${relationName}: ${tableName}RelatedTable;`; } return result; } function columns(table) { let result = ''; let separator = ''; for (let i = 0; i < table._columns.length; i++) { let column = table._columns[i]; result += `${separator}${column.alias} : ${column.tsType};`; separator = ` `; } return result; } function Concurrency(table, name, isRoot) { name = pascalCase(name); if (!isRoot) { if (tablesAdded.has(table)) return ''; else { tablesAdded.set(table, name); } } let otherConcurrency = ''; let concurrencyRelations = ''; let strategyRelations = ''; let regularRelations = ''; let relations = table._relations; let relationName; let separator = ` `; let visitor = {}; visitor.visitJoin = function(relation) { const tableTypeName = getTableName(relation, relationName); otherConcurrency += `${Concurrency(relation.childTable, tableTypeName)}`; concurrencyRelations += `${relationName}?: ${tableTypeName}Concurrency;${separator}`; strategyRelations += `${relationName}?: ${tableTypeName}Strategy | boolean;${separator}`; regularRelations += `${relationName}?: ${tableTypeName} | null;${separator}`; }; visitor.visitOne = visitor.visitJoin; visitor.visitMany = function(relation) { const tableTypeName = getTableName(relation, relationName); otherConcurrency += `${Concurrency(relation.childTable, tableTypeName)}`; concurrencyRelations += `${relationName}?: ${tableTypeName}Concurrency;${separator}`; strategyRelations += `${relationName}?: ${tableTypeName}Strategy | boolean;${separator}`; regularRelations += `${relationName}?: ${tableTypeName}[] | null;${separator}`; }; for (relationName in relations) { var relation = relations[relationName]; relation.accept(visitor); } let row = ''; if (!isRoot) { row = `export interface ${name}RelatedTable { ${columns(table)} ${tableRelations(table)} all: (selector: (table: ${name}RelatedTable) => RawFilter) => Filter; any: (selector: (table: ${name}RelatedTable) => RawFilter) => Filter; none: (selector: (table: ${name}RelatedTable) => RawFilter) => Filter; exists: () => Filter; }`; } return ` export interface ${name}Concurrency { readonly?: boolean; concurrency?: Concurrency; ${concurrencyColumns(table)} ${concurrencyRelations} } export interface ${name} { ${regularColumns(table)} ${regularRelations} } export interface ${name}TableBase { ${columns(table)} ${tableRelations(table)} } export interface ${name}Strategy { ${strategyColumns(table)} ${strategyRelations} limit?: number; offset?: number; orderBy?: Array<${orderByColumns(table)}> | ${orderByColumns(table)}; where?: (table: ${name}TableBase) => RawFilter; } ${otherConcurrency} ${row}`; } function getTableName(relation, relationName) { let name = rootTablesAdded.get(relation.childTable); if (name) return name; else { let name = pascalCase(relationName); let count = 2; while (tableNames.has(name)) { name = name + 'x' + count; count++; } rootTablesAdded.set(relation.childTable, name); tableNames.add(name); return name; } } } function regularColumns(table) { let result = ''; let separator = ''; for (let i = 0; i < table._columns.length; i++) { let column = table._columns[i]; if (column._notNull) result += `${separator}${column.alias} : ${typeMap[column.tsType]};`; else result += `${separator}${column.alias}? : ${typeMap[column.tsType]} | null;`; separator = ` `; } return result; } function orderByColumns(table) { let result = ''; let separator = ''; for (let i = 0; i < table._columns.length; i++) { let column = table._columns[i]; result += `${separator}'${column.alias}' | '${column.alias} desc'`; separator = '| '; } return result; } function pascalCase(name) { return name[0].toUpperCase() + name.substr(1); } function concurrencyColumns(table) { let result = ''; let separator = ''; for (let i = 0; i < table._columns.length; i++) { let column = table._columns[i]; result += `${separator}${column.alias}? : ColumnConcurrency;`; separator = ` `; } return result; } function strategyColumns(table) { let primarySet = new Set(table._primaryColumns); let result = ''; let separator = ''; for (let i = 0; i < table._columns.length; i++) { let column = table._columns[i]; if (primarySet.has(column)) continue; result += `${separator}${column.alias}? : boolean;`; separator = ` `; } return result; } function getCustomFilters(filters) { return getLeafNames(filters); function getLeafNames(obj, tabs = '\t\t\t\t\t') { let result = ''; for (let p in obj) { if (typeof obj[p] === 'object' && obj[p] !== null) { result += '\n' + tabs + p + ': {' + tabs + getLeafNames(obj[p], tabs + '\t'); result += '\n' + tabs + '}'; } else if (typeof obj[p] === 'function') result += '\n' + tabs + p + ': (' + getParamNames(obj[p]) + ') => import(\'orange-orm\').Filter;'; } return result; } } function getParamNames(func) { let STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; let ARGUMENT_NAMES = /([^\s,]+)/g; let fnStr = func.toString().replace(STRIP_COMMENTS, ''); let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES); if (result === null) return ''; return result.slice(1).join(': unknown, ') + ': unknown'; } function getPrefixTs(isNamespace) { if (isNamespace) return ` /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { AxiosInterceptorManager, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; import type { BooleanColumn, JSONColumn, UUIDColumn, DateColumn, NumberColumn, BinaryColumn, StringColumn, Concurrency, Filter, RawFilter, TransactionOptions, Pool, Express, Hono, Url, ColumnConcurrency, JsonPatch } from 'orange-orm'; export { RequestHandler } from 'express'; export { Concurrency, Filter, RawFilter, Config, TransactionOptions, Pool } from 'orange-orm'; export = r; declare function r(config: Config): r.RdbClient; `; return ` /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import schema from './schema'; import type { AxiosInterceptorManager, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; import type { BooleanColumn, JSONColumn, UUIDColumn, DateColumn, NumberColumn, BinaryColumn, StringColumn, Concurrency, Filter, RawFilter, TransactionOptions, Pool, Express, Hono, Url, ColumnConcurrency, JsonPatch } from 'orange-orm'; export default schema as RdbClient;`; } function startNamespace(tables, isHttp) { return ` declare namespace r {${getTables(isHttp)} `; function getTables(isHttp) { let result = ''; for (let name in tables) { let Name = name.substring(0, 1).toUpperCase() + name.substring(1); result += ` const ${name}: ${Name}Table;`; } if (!isHttp) result += ` function and(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; function or(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; function not(): Filter; function transaction(fn: (transaction: RdbClient) => Promise<unknown>, options?: TransactionOptions): Promise<void>; function query(filter: RawFilter | string): Promise<unknown[]>; function query<T>(filter: RawFilter | string): Promise<T[]>; function transaction(fn: (transaction: RdbClient) => Promise<unknown>, options?: TransactionOptions): Promise<void>; const filter: Filter; function express(): Express; function express(config: ExpressConfig): Express; function hono(): Hono; function hono(config: HonoConfig): Hono; `; else result += ` const interceptors: { request: AxiosInterceptorManager<InternalAxiosRequestConfig>; response: AxiosInterceptorManager<AxiosResponse>; }; function reactive(proxyMethod: (obj: unknown) => unknown): void; function and(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; function or(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; function not(): Filter; const filter: Filter; `; return result; } } function getRdbClientTs(tables, isHttp) { return ` export interface RdbClient {${getTables(isHttp)} } export interface RdbConfig { db?: Pool | (() => Pool); readonly?: boolean; concurrency?: Concurrency;${getConcurrencyTables()} } export interface MetaData { readonly?: boolean; concurrency?: Concurrency;${getConcurrencyTables()} } export interface ExpressConfig { db?: Pool | (() => Pool); tables?: ExpressTables; concurrency?: Concurrency; readonly?: boolean; disableBulkDeletes?: boolean; hooks?: ExpressHooks; } export interface HonoConfig { db?: Pool | (() => Pool); tables?: HonoTables; concurrency?: Concurrency; readonly?: boolean; disableBulkDeletes?: boolean; hooks?: HonoHooks; } export interface ExpressContext { request: import('express').Request; response: import('express').Response; client: RdbClient; } export interface HonoRequest { method: string; query: Record<string, string>; headers: Record<string, string>; json(): Promise<unknown>; } export interface HonoResponse { status(code: number): HonoResponse; setHeader(name: string, value: string): HonoResponse; json(value: unknown): unknown; send(value: unknown): unknown; } export interface HonoContext { request: HonoRequest; response: HonoResponse; client: RdbClient; } export interface ExpressTransactionHooks { beforeBegin?: (db: Pool, request: import('express').Request, response: import('express').Response) => void | Promise<void>; afterBegin?: (db: Pool, request: import('express').Request, response: import('express').Response) => void | Promise<void>; beforeCommit?: (db: Pool, request: import('express').Request, response: import('express').Response) => void | Promise<void>; afterCommit?: (db: Pool, request: import('express').Request, response: import('express').Response) => void | Promise<void>; afterRollback?: (db: Pool, request: import('express').Request, response: import('express').Response, error?: unknown) => void | Promise<void>; } export interface ExpressHooks extends ExpressTransactionHooks { transaction?: ExpressTransactionHooks; } export interface HonoTransactionHooks { beforeBegin?: (db: Pool, request: HonoRequest, response: HonoResponse) => void | Promise<void>; afterBegin?: (db: Pool, request: HonoRequest, response: HonoResponse) => void | Promise<void>; beforeCommit?: (db: Pool, request: HonoRequest, response: HonoResponse) => void | Promise<void>; afterCommit?: (db: Pool, request: HonoRequest, response: HonoResponse) => void | Promise<void>; afterRollback?: (db: Pool, request: HonoRequest, response: HonoResponse, error?: unknown) => void | Promise<void>; } export interface HonoHooks extends HonoTransactionHooks { transaction?: HonoTransactionHooks; } export interface ExpressTables {${getExpressTables()} } export interface HonoTables {${getHonoTables()} } `; function getConcurrencyTables() { let result = ''; for (let name in tables) { let Name = name.substring(0, 1).toUpperCase() + name.substring(1); result += ` ${name}?: ${Name}Concurrency;`; } return result; } function getTables(isHttp) { let result = ''; for (let name in tables) { let Name = name.substring(0, 1).toUpperCase() + name.substring(1); result += ` ${name}: ${Name}Table;`; } if (isHttp) result += ` (config: {db: Url}): RdbClient; interceptors: { request: AxiosInterceptorManager<InternalAxiosRequestConfig>; response: AxiosInterceptorManager<AxiosResponse>; }; reactive(proxyMethod: (obj: unknown) => unknown): void; and(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; or(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; not(): Filter; transaction(fn: (transaction: RdbClient) => Promise<unknown>, options?: TransactionOptions): Promise<void>; filter: Filter; createPatch(original: any[], modified: any[]): JsonPatch; createPatch(original: any, modified: any): JsonPatch;`; else result += ` (config: RdbConfig): RdbClient; and(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; or(filter: RawFilter | RawFilter[], ...filters: RawFilter[]): Filter; not(): Filter; query(filter: RawFilter | string): Promise<unknown[]>; query<T>(filter: RawFilter | string): Promise<T[]>; transaction(fn: (transaction: RdbClient) => Promise<unknown>, options?: TransactionOptions): Promise<void>; filter: Filter; createPatch(original: any[], modified: any[]): JsonPatch; createPatch(original: any, modified: any): JsonPatch; express(): Express; express(config: ExpressConfig): Express; hono(): Hono; hono(config: HonoConfig): Hono; readonly metaData: MetaData;`; return result; } function getExpressTables() { let result = ''; for (let name in tables) { let Name = name.substring(0, 1).toUpperCase() + name.substring(1); result += ` ${name}?: boolean | ${Name}ExpressConfig;`; } return result; } function getHonoTables() { let result = ''; for (let name in tables) { let Name = name.substring(0, 1).toUpperCase() + name.substring(1); result += ` ${name}?: boolean | ${Name}HonoConfig;`; } return result; } } getTSDefinition_1 = getTSDefinition; return getTSDefinition_1; } var getMeta_1; var hasRequiredGetMeta; function requireGetMeta () { if (hasRequiredGetMeta) return getMeta_1; hasRequiredGetMeta = 1; function getMeta(table, map = new Map()) { if (map.has(table)) return map.get(table).id; let strategy = { keys: table._primaryColumns.map(x => ({name: x.alias, type: x.tsType})), columns: {}, relations: {}, id: map.size }; map.set(table, strategy); for (let i = 0; i < table._columns.length; i++) { const column = table._columns[i]; strategy.columns[column.alias] = {}; if ('serializable' in column && !column.serializable) strategy.columns[column.alias].serializable = false; else strategy.columns[column.alias].serializable = true; } let relations = table._relations; let relationName; let visitor = {}; visitor.visitJoin = function(relation) { strategy.relations[relationName] = getMeta(relation.childTable, map); }; visitor.visitMany = function(relation) { strategy.relations[relationName] = getMeta(relation.childTable, map); }; visitor.visitOne = visitor.visitMany; for (relationName in relations) { let relation = relations[relationName]; relation.accept(visitor); } return strategy; } getMeta_1 = getMeta; return getMeta_1; } var dateToISOString_1; var hasRequiredDateToISOString; function requireDateToISOString () { if (hasRequiredDateToISOString) return dateToISOString_1; hasRequiredDateToISOString = 1; function dateToISOString(date) { let tzo = -date.getTimezoneOffset(); let dif = tzo >= 0 ? '+' : '-'; function pad(num) { let norm = Math.floor(Math.abs(num)); return (norm < 10 ? '0' : '') + norm; } function padMilli(d) { return (d.getMilliseconds() + '').padStart(3, '0'); } return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()) + '.' + padMilli(date) + dif + pad(tzo / 60) + ':' + pad(tzo % 60); } dateToISOString_1 = dateToISOString; return dateToISOString_1; } var stringify_1; var hasRequiredStringify; function requireStringify () { if (hasRequiredStringify) return stringify_1; hasRequiredStringify = 1; let dateToISOString = requireDateToISOString(); function stringify(value) { return JSON.stringify(value, replacer); } function replacer(key, value) { // @ts-ignore if (typeof value === 'bigint') return value.toString(); else if (value instanceof Date && !isNaN(value)) return dateToISOString(value); else return value; } stringify_1 = stringify; return stringify_1; } var sync; var hasRequiredSync; function requireSync () { if (hasRequiredSync) return sync; hasRequiredSync = 1; const stringify = requireStringify(); function newSyncHandler(client, options = {}) { const syncOptions = normalizeSyncOptions(options.sync); if (!syncOptions || syncOptions.enabled === false) return null; const tableMeta = createTableMeta(client, syncOptions); const queue = createQueue(syncOptions.queue); return async function handleSync(request, response) { try { const result = await queue.run(() => execute(request.body || {})); response.json(result); } catch (e) { if (e.status === undefined) response.status(500).send(e.message || e); else response.status(e.status).send(e.message); } }; async function execute(body) { const phase = body.phase || body.action; if (phase === 'push') return pushMutations(body); if (phase === 'keys') return pullKeys(body); if (phase === 'rows') return pullRows(body); const error = new Error('Invalid sync phase. Use { phase: "keys" }, { phase: "rows" }, or { phase: "push" }.'); error.status = 400; throw error; } async function pushMutations(body) { const clientId = normalizeClientId(body.clientId ?? body.client_id); const mutations = normalizeMutations(body.mutations, syncOptions.limits.maxMutationsPerBatch); if (!clientId) { const error = new Error('Sync push requires "clientId".'); error.status = 400; throw error; } if (mutations.length === 0) { return { phase: 'push', applied: 0, duplicates: 0, results: [] }; } const results = []; let applied = 0; let duplicates = 0; for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i]; await client.transaction(async (tx) => { const claim = await claimAppliedMutation(tx, clientId, mutation.id); if (!claim.claimed) { duplicates += 1; results.push({ id: mutation.id, table: mutation.table, ...(claim.result || {}), duplicate: true }); return; } const patchResult = await applyMutationPatches(tx, mutation); const result = { id: mutation.id, table: mutation.table, applied: true, changed: patchResult.changed, result: patchResult }; await updateAppliedMutation(tx, clientId, mutation.id, result); results.push(result); applied += 1; }); } return { phase: 'push', applied, duplicates, results }; } async function applyMutationPatches(tx, mutation) { const entries = Array.isArray(mutation.patches) ? mutation.patches : [{ table: mutation.table, patch: mutation.patch, options: mutation.options }]; let changed = 0; const results = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const table = tx[entry.table]; if (!table || typeof table.patch !== 'function') { const error = new Error(`Table "${entry.table}" is not exposed or does not exist`); error.status = 400; throw error; } const result = await table.patch(entry.patch, entry.options || mutation.options || {}); changed += Array.isArray(result && result.changed) ? result.changed.length : 0; results.push({ table: entry.table, result }); } return { changed, results }; } async function pullKeys(body) { const requestedTables = normalizeRequestedTables(body.tables, tableMeta, syncOptions.limits.maxTablesPerRequest); const limit = normalizeLimit(body.limit, syncOptions.limits.maxKeysPerBatch); const token = normalizeToken(body.token, requestedTables); if (token && token.mode === 'changes') return pullKeysFromChanges(token, limit); if (token && token.mode === 'snapshot') return pullKeysFromSnapshot(token, limit); const startCursor = normalizeCursor(body.cursor ?? body.since); const bounds = await getChangeBounds(syncOptions.changeTable); const fallback = shouldUseSnapshot(startCursor, bounds, syncOptions.limits.maxChangeWindow); if (fallback.useSnapshot) { const snapshotToken = { v: 1, mode: 'snapshot', tables: requestedTables, tableIndex: 0, offset: 0, watermark: bounds.max }; const result = await pullKeysFromSnapshot(snapshotToken, limit); result.reason = fallback.reason; return result; } const changeToken = { v: 1, mode: 'changes', tables: requestedTables, cursor: startCursor, watermark: bounds.max }; return pullKeysFromChanges(changeToken, limit); } async function pullKeysFromSnapshot(token, limit) { const items = []; let tableIndex = normalizeInteger(token.tableIndex, 0); let offset = normalizeInteger(token.offset, 0); while (items.length < limit && tableIndex < token.tables.length) { const tableName = token.tables[tableIndex]; const meta = tableMeta.byName.get(tableName); if (!meta) { tableIndex += 1; offset = 0; continue; } const remaining = limit - items.length; const keys = await fetchSnapshotKeys(meta, remaining, offset); for (let i = 0; i < keys.length; i++) { const pk = keys[i]; items.push({ table: tableName, pk, key: toKeyObject(meta, pk), op: 'U' }); } if (keys.length < remaining) { tableIndex += 1; offset = 0; } else { offset += keys.length; } } const done = tableIndex >= token.tables.length; return { phase: 'keys', mode: 'snapshot', done, cursor: token.watermark, token: done ? null : { v: 1, mode: 'snapshot', tables: token.tables, tableIndex, offset, watermark: token.watermark }, items }; } async function pullKeysFromChanges(token, limit) { const fromCursor = normalizeInteger(token.cursor, 0); const watermark = normalizeInteger(token.watermark, 0); if (fromCursor >= watermark) { return { phase: 'keys', mode: 'changes', done: true, cursor: watermark, token: null, items: [] }; } const whereTables = token.tables.length === 0 ? '' : ` AND table_name IN (${token.tables.map((name) => sqlStringLiteral(tableMeta.byName.get(name).dbName)).join(',')})`; const sql = [ 'SELECT id, table_name, op, pk_json', `FROM ${quoteQualified(syncOptions.changeTable)}`, `WHERE id > ${fromCursor} AND id <= ${watermark}${whereTables}`, 'ORDER BY id ASC', `LIMIT ${limit}` ].join(' '); const rows = await safeQuery(sql, []); const dedup = new Map(); let nextCursor = fromCursor; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const id = normalizeInteger(row.id ?? row.ID, nextCursor); nextCursor = id > nextCursor ? id : nextCursor; const rawTableName = row.table_name ?? row.TABLE_NAME; const meta = tableMeta.byDbName.get(rawTableName); if (!meta) continue; let keyObject; try { keyObject = typeof row.pk_json === 'string' ? JSON.parse(row.pk_json) : JSON.parse(row.PK_JSON); } catch (_e) { continue; } const pk = toPkArray(meta, keyObject); if (!pk) continue; const mapKey = `${meta.name}|${stringify(pk)}`; const op = normalizeOp(row.op ?? row.OP); if (dedup.has(mapKey)) dedup.delete(mapKey); dedup.set(mapKey, { table: meta.name, pk, key: toKeyObject(meta, pk), op }); } const items = Array.from(dedup.values()); const done = rows.length === 0 || nextCursor >= watermark; return { phase: 'keys', mode: 'changes', done, cursor: watermark, token: done ? null : { v: 1, mode: 'changes', tables: token.tables, cursor: nextCursor, watermark }, items }; } async function pullRows(body) { const rawItems = Array.isArray(body.items) ? body.items : []; const limit = normalizeLimit(rawItems.length, syncOptions.limits.maxRowsPerBatch); const items = rawItems.slice(0, limit); const tableKeys = new Map(); for (let i = 0; i < items.length; i++) { const item = items[i]; if (!item || typeof item.table !== 'string') continue; const meta = tableMeta.byName.get(item.table); if (!meta) continue; if (normalizeOp(item.op) === 'D') continue; const pk = Array.isArray(item.pk) ? item.pk : toPkArray(meta, item.key); if (!pk || pk.length !== meta.pkColumns.length) continue; if (!tableKeys.has(meta.name)) tableKeys.set(meta.name, []); tableKeys.get(meta.name).push(pk); } const rowMap = new Map(); const tableNames = Array.from(tableKeys.keys()); for (let i = 0; i < tableNames.length; i++) { const tableName = tableNames[i]; const meta = tableMeta.byName.get(tableName); const keys = tableKeys.get(tableName); const rows = await fetchRowsByPrimaryKeys(meta, keys); const perTable = new Map(); for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; const pk = toPkArray(meta, row); perTable.set(stringify(pk), row); } rowMap.set(tableName, perTable); } const resolved = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (!item || typeof item.table !== 'string') continue; const meta = tableMeta.byName.get(item.table); if (!meta) continue; const pk = Array.isArray(item.pk) ? item.pk : toPkArray(meta, item.key); if (!pk) continue; const row = rowMap.get(meta.name)?.get(stringify(pk)); if (row !== undefined) resolved.push({ table: meta.name, pk, key: toKeyObject(meta, pk), row, op: normalizeOp(item.op) }); } return { phase: 'rows', items: resolved }; } async function fetchSnapshotKeys(meta, limit, offset) { const strategy = {}; for (let i = 0; i < meta.pkColumns.length; i++) { strategy[meta.pkColumns[i].alias] = true; } strategy.orderBy = meta.pkColumns.map(x => x.alias); strategy.limit = limit; strategy.offset = offset; const rows = await client.transaction(async (tx) => { return tx[meta.name].getMany(undefined, strategy); }, { readonly: true }); const result = []; for (let i = 0; i < rows.length; i++) { const pk = toPkArray(meta, rows[i]); if (pk) result.push(pk); } return result; } async function fetchRowsByPrimaryKeys(meta, keys) { if (!Array.isArray(keys) || keys.length === 0) return []; const where = []; const parameters = []; for (let i = 0; i < keys.length; i++) { const pk = keys[i]; if (!Array.isArray(pk) || pk.length !== meta.pkColumns.length) continue; const parts = []; for (let colIndex = 0; colIndex < meta.pkColumns.length; colIndex++) { const col = meta.pkColumns[colIndex]; parts.push(`${quoteIdent(col.dbName)} = ?`); parameters.push(pk[colIndex]); } where.push(`(${parts.join(' AND ')})`); } if (where.length === 0) return []; const filter = { sql: where.join(' OR '), parameters }; return client.transaction(async (tx) => { return tx[meta.name].getMany(filter); }, { readonly: true }); } async function getChangeBounds(changeTable) { try { const sql = [ 'SELECT', 'COALESCE(MIN(id), 0) AS min_id,', 'COALESCE(MAX(id), 0) AS max_id', `FROM ${quoteQualified(changeTable)}` ].join(' '); const rows = await safeQuery(sql, []); const row = rows[0] || {}; return { exists: true, min: normalizeInteger(row.min_id ?? row.MIN_ID, 0), max: normalizeInteger(row.max_id ?? row.MAX_ID, 0) }; } catch (_error) { return { exists: false, min: 0, max: 0 }; } } async function safeQuery(sql, fallback) { const result = await client.query(sql); if (Array.isArray(result)) return result; if (Array.isArray(result?.rows)) return result.rows; return fallback; } async function claimAppliedMutation(tx, clientId, mutationId) { const rows = await safeTxQuery(tx, [ `INSERT INTO ${quoteQualified(syncOptions.appliedMutationsTable)} (client_id, mutation_id, result_json)`, `VALUES (${sqlStringLiteral(clientId)}, ${sqlStringLiteral(mutationId)}, ${sqlJsonLiteral({ pending: true })})`, 'ON CONFLICT (client_id, mutation_id) DO NOTHING', 'RETURNING result_json' ].join(' '), []); if (rows.length > 0) return { claimed: true }; const existingRows = await safeTxQuery(tx, [ 'SELECT result_json', `FROM ${quoteQualified(syncOptions.appliedMutationsTable)}`, `WHERE client_id = ${sqlStringLiteral(clientId)} AND mutation_id = ${sqlStringLiteral(mutationId)}`, 'LIMIT 1' ].join(' '), []); const result = parseResultJson(existingRows[0]); return { claimed: false, result }; } function parseResultJson(row) { if (!row) return null; const raw = row.result_json ?? row.RESULT_JSON; if (typeof raw === 'string') { try { return JSON.parse(raw); } catch (_e) { return null; } } return raw && raw === Object(raw) ? raw : null; } async function updateAppliedMutation(tx, clientId, mutationId, result) { await tx.query([ `UPDATE ${quoteQualified(syncOptions.appliedMutationsTable)}`, `SET result_json = ${sqlJsonLiteral(result)}, applied_at = NOW()`, `WHERE client_id = ${sqlStringLiteral(clientId)} AND mutation_id = ${sqlStringLiteral(mutationId)}` ].join(' ')); } async function safeTxQuery(tx, sql, fallback) { const result = await tx.query(sql); if (Array.isArray(result)) return result; if (Array.isArray(result?.rows)) return result.rows; return fallback; } } function normalizeSyncOptions(sync) { if (!sync) return null; const queueOptions = sync.queue || {}; const limits = sync.limits || {}; return { enabled: sync.enabled !== false, changeTable: sync.changeTable || 'orange_changes', appliedMutationsTable: sync.appliedMutationsTable || 'orange_sync_applied_mutations', queue: { concurrency: clamp(normalizeInteger(queueOptions.concurrency, 4), 1, 100), maxPending: clamp(normalizeInteger(queueOptions.maxPending, 1000), 0, 100000) }, limits: { maxTablesPerRequest: clamp(normalizeInteger(limits.maxTablesPerRequest, 50), 1, 1000), maxKeysPerBatch: clamp(normalizeInteger(limits.maxKeysPerBatch, 200), 1, 10000), maxRowsPerBatch: clamp(normalizeInteger(limits.maxRowsPerBatch, 200), 1, 10000), maxMutationsPerBatch: clamp(normalizeInteger(limits.maxMutationsPerBatch, 200), 1, 10000), maxChangeWindow: clamp(normalizeInteger(limits.maxChangeWindow, 50000), 1, 100000000) } }; } function createTableMeta(client, syncOptions) { const byName = new Map(); const byDbName = new Map(); for (let tableName in client.tables) { const table = client.tables[tableName]; const pkColumns = Array.isArray(table?._primaryColumns) ? table._primaryColumns : []; if (pkColumns.length === 0) continue; const dbName = table._dbName; if (!dbName || dbName === syncOptions.changeTable) continue; const meta = { name: tableName, dbName, pkColumns: pkColumns.map((col) => ({ alias: col.alias, dbName: col._dbName || col.alias })) }; byName.set(tableName, meta); byDbName.set(dbName, meta); const split = dbName.split('.'); byDbName.set(split[split.length - 1], meta); } return { byName, byDbName }; } function createQueue({ concurrency, maxPending }) { let running = 0; const pending = []; return { run }; function run(job) { return new Promise((resolve, reject) => { if (running >= concurrency && pending.length >= maxPending) { const error = new Error('Sync queue is full. Try again later.'); error.status = 429; reject(error); return; } pending.push({ job, resolve, reject }); drain(); }); } function drain() { while (running < concurrency && pending.length > 0) { const next = pending.shift(); running += 1; Promise.resolve() .then(next.job) .then(next.resolve, next.reject) .finally(() => { running -= 1; drain(); }); } } } function shouldUseSnapshot(cursor, bounds, maxChangeWindow) { if (!Number.isFinite(cursor)) return { useSnapshot: true, reason: 'first_sync' }; if (!bounds.exists) return { useSnapshot: true, reason: 'change_table_unavailable' }; if (cursor < bounds.min - 1) return { useSnapshot: true, reason: 'cursor_too_old' }; if (bounds.max - cursor > maxChangeWindow) return { useSnapshot: true, reason: 'cursor_too_far_behind' }; return { useSnapshot: false }; } function normalizeRequestedTables(rawTables, tableMeta, maxTablesPerRequest) { if (!Array.isArray(rawTables) || rawTables.length === 0) return Array.from(tableMeta.byName.keys()); const normalized = []; for (let i = 0; i < rawTables.length; i++) { const raw = rawTables[i]; if (typeof raw !== 'string') continue; const byName = tableMeta.byName.get(raw); if (byName) { normalized.push(byName.name); continue; } const byDbName = tableMeta.byDbName.get(raw); if (byDbName) normalized.push(byDbName.name); } const deduped = Array.from(new Set(normalized)); return deduped.slice(0, maxTablesPerRequest); } function normalizeToken(token, requestedTables) { if (!token || token !== Object(token)) return null; if (token.v !== 1) return null; if (token.mode === 'changes') { return { v: 1, mode: 'changes', tables: requestedTables, cursor: normalizeInteger(token.cursor, 0), watermark: normalizeInteger(token.watermark, 0) }; } if (token.mode === 'snapshot') { return { v: 1, mode: 'snapshot', tables: requestedTables, tableIndex: normalizeInteger(token.tableIndex, 0), offset: normalizeInteger(token.offset, 0), watermark: normalizeInteger(token.watermark, 0) }; } return null; } function normalizeCursor(cursor) { if (cursor === null || cursor === undefined || cursor === '') return NaN; if (typeof cursor === 'number' && Number.isFinite(cursor)) return cursor; if (typeof cursor === 'string') { const parsed = Number.parseInt(cursor, 10); return Number.isFinite(parsed) ? parsed : NaN; } return NaN; } function normalizeLimit(limit, max) { return clamp(normalizeInteger(limit, max), 1, max); } function normalizeInteger(value, fallback) { if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value); if (typeof value === 'string') { const parsed = Number.parseInt(value, 10); if (Number.isFinite(parsed)) return parsed; } return fallback; } function normalizeOp(value) { if (typeof value !== 'string') return 'U'; const op = value.toUpperCase(); if (op === 'I' || op === 'U' || op === 'D') return op; return 'U'; } function normalizeClientId(value) { if (typeof value !== 'string') return ''; return value.trim(); } function normalizeMutations(value, limit) { if (!Array.isArray(value)) return []; const result = []; for (let i = 0; i < value.length && result.length < limit; i++) { const mutation = normalizeMutation(value[i]); if (mutation) result.push(mutation); } return result; } function normalizeMutation(value) { if (!value || value !== Object(value)) return null; const id = value.id ?? value.mutationId ?? value.mutation_id; if (typeof id !== 'string' || id.length === 0) return null; if (Array.isArray(value.patches)) { const patches = value.patches.map(normalizeMutationPatch).filter(Boolean); if (patches.length === 0) return null; return { id, patches, options: value.options && value.options === Object(value.options) ? value.options : undefined }; } const entry = normalizeMutationPatch(value); if (!entry) return null; return { id, ...entry, options: value.options && value.options === Object(value.options) ? value.options : undefined }; } function normalizeMutationPatch(value) { if (!value || value !== Object(value)) return null; if (typeof value.table !== 'string' || value.table.length === 0) return null; if (!Array.isArray(value.patch)) return null; return { table: value.table, patch: value.patch, options: value.options && value.options === Object(value.options) ? value.options : undefined }; } function toPkArray(meta, row) { if (!row || row !== Object(row)) return null; const result = []; for (let i = 0; i < meta.pkColumns.length; i++) { const key = meta.pkColumns[i].alias; if (!(key in row)) return null; result.push(row[key]); } return result; } function toKeyObject(meta, pk) { const key = {}; for (let i = 0; i < meta.pkColumns.length && i < pk.length; i++) { key[meta.pkColumns[i].alias] = pk[i]; } return key; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function quoteQualified(name) { return String(name).split('.').map(quoteIdent).join('.'); } function quoteIdent(name) { return `"${String(name).replace(/"/g, '""')}"`; } function sqlStringLiteral(value) { return `'${String(value).replace(/'/g, '\'\'')}'`; } function sqlJsonLiteral(value) { return `${sqlStringLiteral(stringify(value))}::jsonb`; } sync = newSyncHandler; return sync; } var hostExpress_1; var hasRequiredHostExpress; function requireHostExpress () { if (hasRequiredHostExpress) return hostExpress_1; hasRequiredHostExpress = 1; const getTSDefinition = requireGetTSDefinition(); // let hostLocal = _hostLocal; const getMeta = requireGetMeta(); const newSyncHandler = requireSync(); function hostExpress(hostLocal, client, options = {}) { if ('db' in options && (options.db ?? undefined) === undefined || !client.db) throw new Error('No db specified'); const dbOptions = { db: options.db || client.db }; let c = {}; const readonly = { readonly: options.readonly}; const sharedHooks = options.hooks; for (let tableName in client.tables) { const tableOptions = options[tableName] || {}; const hooks = tableOptions.hooks || sharedHooks; c[tableName] = hostLocal({ ...dbOptions, ...readonly, ...tableOptions, table: client.tables[tableName], isHttp: true, client, hooks }); } const syncHandler = newSyncHandler(client, options); async function handler(req, res) { if (req.method === 'POST') return post.apply(null, arguments); if (req.method === 'PATCH') return patch.apply(null, arguments); if (req.method === 'GET') return get.apply(null, arguments); if (req.method === 'OPTIONS') return handleOptions(req, res); // assuming the second argument is `response` else res.status(405).set('Allow', 'GET, POST, PATCH, OPTIONS').send('Method Not Allowed'); } handler.db = handler; handler.dts = get; function get(request, response) { try { if (request.query.table) { if (!(request.query.table in c)) { let e = new Error('Table is not exposed or does not exist'); // @ts-ignore e.status = 400; throw e; } const result = getMeta(client.tables[request.query.table]); response.setHeader('content-type', 'text/plain'); response.status(200).send(result); } else { const isNamespace = request.query.isNamespace === 'true'; let tsArg = Object.keys(c).map(x => { return { table: client.tables[x], customFilters: options?.tables?.[x].customFilters, name: x }; }); response.setHeader('content-type', 'text/plain'); response.status(200).send(getTSDefinition(tsArg, { isNamespace, isHttp: true })); } } catch (e) { if (e.status === undefined) response.status(500).send(e.message || e); else response.status(e.status).send(e.message); } } async function patch(request, response) { try { response.json(await c[request.query.table].patch(request.body, request, response)); } catch (e) { if (e.status === undefined) response.status(500).send(e.message || e); else response.status(e.status).send(e.message); } } async function post(request, response) { try { if (request.query.sync) { if (!syncHandler) { const e = new Error('Sync is not enabled for this endpoint'); // @ts-ignore e.status = 404; throw e; } return syncHandler(request, response); } if (!request.query.table) { let e = new Error('Table not defined'); // @ts-ignore e.status = 400; throw e; } else if (!(request.query.table in c)) { let e = new Error('Table is not exposed or does not exist'); // @ts-ignore e.status = 400; throw e; } response.json(await c[request.query.table].post(request.body, request, response)); } catch (e) { if (e.status === undefined) response.status(500).send(e.message || e); else response.status(e.status).send(e.message); } } function handleOptions(req, response) { response.setHeader('Access-Control-Allow-Origin', '*'); // Adjust this as per your CORS needs response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS'); // And any other methods you support response.setHeader('Access-Control-Allow-Headers', 'Content-Type'); // And any other headers you expect in requests response.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight request for a day. Adjust as you see fit response.status(204).send(); // 204 No Content response for successful OPTIONS requests } return handler; } hostExpress_1 = hostExpress; return hostExpress_1; } var hostHono_1; var hasRequiredHostHono; function requireHostHono () { if (hasRequiredHostHono) return hostHono_1; hasRequiredHostHono = 1; const getTSDefinition = requireGetTSDefinition(); const getMeta = requireGetMeta(); function hostHono(hostLocal, client, options = {}) { if ('db' in options && (options.db ?? undefined) === undefined || !client.db) throw new Error('No db specified'); const dbOptions = { db: options.db || client.db }; let c = {}; const readonly = { readonly: options.readonly }; const sharedHooks = options.hooks; for (let tableName in client.tables) { const tableOptions = options[tableName] || {}; const hooks = tableOptions.hooks || sharedHooks; c[tableName] = hostLocal({ ...dbOptions, ...readonly, ...tableOptions, table: client.tables[tableName], isHttp: true, client, hooks }); } async function handler(ctx) { const request = createRequest(ctx); const response = createResponse(); try { if (request.method === 'POST')