uql-orm 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/browser/uql-browser.min.js.map +3 -3
  2. package/dist/dialect/abstractSqlDialect.d.ts +14 -1
  3. package/dist/dialect/abstractSqlDialect.js +21 -14
  4. package/dist/dialect/mysqlLikeSqlDialect.d.ts +11 -1
  5. package/dist/dialect/mysqlLikeSqlDialect.js +25 -0
  6. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -7
  7. package/dist/dialect/pgLikeSqlDialect.js +7 -12
  8. package/dist/entity/decorator/bag.d.ts +35 -0
  9. package/dist/entity/decorator/bag.js +54 -0
  10. package/dist/entity/decorator/entity.d.ts +28 -2
  11. package/dist/entity/decorator/entity.js +40 -2
  12. package/dist/entity/decorator/members.d.ts +51 -0
  13. package/dist/entity/decorator/members.js +51 -0
  14. package/dist/entity/index.d.ts +3 -1
  15. package/dist/entity/index.js +3 -1
  16. package/dist/entity/metadata/definition.d.ts +23 -9
  17. package/dist/entity/metadata/definition.js +55 -100
  18. package/dist/http/contract.js +1 -2
  19. package/dist/http/handler.js +5 -26
  20. package/dist/http/query.js +1 -1
  21. package/dist/migrate/acquireQuerierForMigrations.d.ts +13 -1
  22. package/dist/migrate/acquireQuerierForMigrations.js +28 -0
  23. package/dist/migrate/cli-config.js +24 -8
  24. package/dist/migrate/cli.js +5 -1
  25. package/dist/migrate/codegen/entityCodeGenerator.js +3 -4
  26. package/dist/migrate/codegen/index.d.ts +0 -1
  27. package/dist/migrate/codegen/index.js +0 -2
  28. package/dist/migrate/drift/driftDetector.d.ts +10 -1
  29. package/dist/migrate/drift/driftDetector.js +22 -5
  30. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -1
  31. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +17 -27
  32. package/dist/migrate/introspection/mongoIntrospector.js +7 -13
  33. package/dist/migrate/migrator.js +44 -81
  34. package/dist/migrate/storage/databaseStorage.d.ts +2 -0
  35. package/dist/migrate/storage/databaseStorage.js +8 -23
  36. package/dist/mongo/mongodbQuerier.js +171 -229
  37. package/dist/nestjs/uqlContextInterceptor.js +58 -22
  38. package/dist/nestjs/uqlModule.d.ts +2 -11
  39. package/dist/nestjs/uqlModule.js +93 -44
  40. package/dist/postgres/postgresDialect.js +2 -2
  41. package/dist/querier/abstractQuerier.d.ts +28 -0
  42. package/dist/querier/abstractQuerier.js +47 -22
  43. package/dist/querier/abstractQuerierPool.d.ts +3 -0
  44. package/dist/querier/abstractQuerierPool.js +5 -3
  45. package/dist/querier/abstractSqlQuerier.d.ts +1 -3
  46. package/dist/querier/abstractSqlQuerier.js +48 -96
  47. package/dist/querier/index.d.ts +3 -1
  48. package/dist/querier/index.js +3 -1
  49. package/dist/querier/querierContext.browser.d.ts +12 -0
  50. package/dist/querier/querierContext.browser.js +18 -0
  51. package/dist/querier/querierContext.d.ts +22 -0
  52. package/dist/querier/querierContext.js +42 -0
  53. package/dist/querier/queryError.d.ts +22 -0
  54. package/dist/querier/queryError.js +20 -0
  55. package/dist/querier/transactional.d.ts +26 -0
  56. package/dist/querier/transactional.js +43 -0
  57. package/dist/schema/index.d.ts +1 -1
  58. package/dist/schema/schemaASTBuilder.d.ts +1 -1
  59. package/dist/schema/schemaASTBuilder.js +2 -2
  60. package/dist/schema/types.d.ts +0 -21
  61. package/dist/sqlite/abstractSqliteQuerier.d.ts +9 -2
  62. package/dist/sqlite/hranaQuerier.js +20 -42
  63. package/dist/sqlite/index.d.ts +1 -0
  64. package/dist/sqlite/index.js +1 -0
  65. package/dist/sqlite/localSqliteQuerierPool.d.ts +31 -0
  66. package/dist/sqlite/localSqliteQuerierPool.js +34 -0
  67. package/dist/sqlite/nodeSqliteAdapter.d.ts +35 -0
  68. package/dist/sqlite/nodeSqliteAdapter.js +28 -0
  69. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +28 -0
  70. package/dist/sqlite/nodeSqliteQuerierPool.js +29 -0
  71. package/dist/sqlite/sqliteDialect.d.ts +1 -2
  72. package/dist/sqlite/sqliteDialect.js +0 -13
  73. package/dist/sqlite/sqliteQuerierPool.d.ts +10 -23
  74. package/dist/sqlite/sqliteQuerierPool.js +9 -28
  75. package/dist/type/entity.d.ts +95 -28
  76. package/dist/type/querier.d.ts +6 -0
  77. package/dist/util/hook.util.js +1 -1
  78. package/package.json +8 -16
  79. package/dist/entity/decorator/field.d.ts +0 -2
  80. package/dist/entity/decorator/field.js +0 -7
  81. package/dist/entity/decorator/filter.d.ts +0 -7
  82. package/dist/entity/decorator/filter.js +0 -11
  83. package/dist/entity/decorator/hook.d.ts +0 -7
  84. package/dist/entity/decorator/hook.js +0 -14
  85. package/dist/entity/decorator/id.d.ts +0 -2
  86. package/dist/entity/decorator/id.js +0 -7
  87. package/dist/entity/decorator/index-decorator.d.ts +0 -22
  88. package/dist/entity/decorator/index-decorator.js +0 -26
  89. package/dist/entity/decorator/index.d.ts +0 -8
  90. package/dist/entity/decorator/index.js +0 -8
  91. package/dist/entity/decorator/relation.d.ts +0 -8
  92. package/dist/entity/decorator/relation.js +0 -19
  93. package/dist/migrate/codegen/smartRelationDetector.d.ts +0 -47
  94. package/dist/migrate/codegen/smartRelationDetector.js +0 -134
  95. package/dist/querier/decorator/index.d.ts +0 -4
  96. package/dist/querier/decorator/index.js +0 -4
  97. package/dist/querier/decorator/injectQuerier.d.ts +0 -3
  98. package/dist/querier/decorator/injectQuerier.js +0 -33
  99. package/dist/querier/decorator/log.d.ts +0 -32
  100. package/dist/querier/decorator/log.js +0 -52
  101. package/dist/querier/decorator/serialized.d.ts +0 -6
  102. package/dist/querier/decorator/serialized.js +0 -13
  103. package/dist/querier/decorator/transactional.d.ts +0 -6
  104. package/dist/querier/decorator/transactional.js +0 -48
@@ -6,12 +6,12 @@
6
6
  "import type { RequestErrorResponse, RequestSuccessResponse } from '../../http/contract.js';\nimport type { RequestOptions } from '../type/index.js';\nimport { notify } from './bus.js';\n\n/**\n * Error thrown for non-2xx responses. Carries the HTTP status so callers can key\n * behavior on it (401 redirects, 402 payment flows, error-boundary routing).\n */\nexport class RequestError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = 'RequestError';\n }\n}\n\nexport function get<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'get' }, opts);\n}\n\nexport function post<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'post', body }, opts);\n}\n\nexport function patch<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'patch', body }, opts);\n}\n\nexport function put<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'put', body }, opts);\n}\n\nexport function remove<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'delete' }, opts);\n}\n\n/**\n * HTTP QUERY (RFC 10008): a safe, idempotent read whose JSON query travels in the\n * request body, avoiding URL-length limits. Method name must stay uppercase\n * (fetch only normalizes the classic verbs).\n */\nexport function query<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'QUERY', body }, opts);\n}\n\nfunction request<T>(url: string, init: RequestInit, opts?: RequestOptions) {\n notify({ phase: 'start', opts });\n\n init.headers = {\n accept: 'application/json',\n 'content-type': 'application/json',\n ...opts?.headers,\n };\n if (opts?.signal) {\n init.signal = opts.signal;\n }\n\n return fetch(url, init)\n .then((rawResp) =>\n rawResp.json().then((resp: unknown) => {\n const isSuccess = rawResp.status >= 200 && rawResp.status < 300;\n if (isSuccess) {\n notify({ phase: 'success', opts });\n return resp as RequestSuccessResponse<T>;\n }\n const errorResp = resp as Partial<RequestErrorResponse> | undefined;\n const error = {\n message: errorResp?.error?.message ?? rawResp.statusText,\n code: errorResp?.error?.code ?? rawResp.status,\n };\n notify({ phase: 'error', error, opts });\n throw new RequestError(error.message, error.code);\n }),\n )\n .finally(() => {\n notify({ phase: 'complete', opts });\n });\n}\n",
7
7
  "import type { FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/** Whether `obj` has at least two enumerable keys. */\nexport function hasMultipleKeys(obj: object): boolean {\n let count = 0;\n for (const _ in obj) {\n if (++count > 1) return true;\n }\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey(obj: object, pred: (key: string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */\nexport function someValue(obj: object, pred: (value: unknown) => boolean): boolean {\n return someKey(obj, (key) => pred((obj as Record<string, unknown>)[key]));\n}\n\nconst isOperatorKey = (key: string) => key.startsWith('$');\n\n/**\n * Whether `value` is a non-empty object whose keys are query/update operators (`$eq`, `$push`, ...).\n * The single source of this test: the SQL dialects, the MongoDB dialect and the `$elemMatch` walker\n * all classify operator objects with it, and they used to disagree about `{}`.\n */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */\nexport function isOperatorOnlyObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));\n}\n\nexport function getKeys<T extends object>(obj: T): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\nexport function getFieldKeys<E>(\n fields: {\n [K in FieldKey<E>]?: FieldOptions;\n },\n): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\n}\n",
8
8
  "export function kebabCase(val: string): string {\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n resp += val[i] === val[i].toUpperCase() ? '-' + val[i].toLowerCase() : val[i];\n }\n return resp;\n}\n\nexport function upperFirst(text: string): string {\n if (!text) return text;\n return text[0].toUpperCase() + text.slice(1);\n}\n\nexport function lowerFirst(text: string): string {\n if (!text) return text;\n return text[0].toLowerCase() + text.slice(1);\n}\n\nexport function snakeCase(val: string): string {\n if (val === null || val === undefined) return val as string;\n if (!val) return '';\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n const char = val[i];\n const charLower = char.toLowerCase();\n if (char !== charLower && char === char.toUpperCase()) {\n resp += '_' + charLower;\n } else {\n resp += char;\n }\n }\n return resp;\n}\n\n/**\n * Convert a string to PascalCase (UpperCamelCase).\n * @example 'user_profile' -> 'UserProfile'\n * @example 'some-text' -> 'SomeText'\n */\nexport function pascalCase(str: string): string {\n if (!str) return '';\n return str\n .split(/[_\\s-]+/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join('');\n}\n\n/**\n * Convert a string to camelCase.\n * @example 'user_profile' -> 'userProfile'\n * @example 'SomeText' -> 'someText'\n */\nexport function camelCase(str: string): string {\n const pascal = pascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/**\n * Simple singularize function for English words.\n * @example 'users' -> 'user'\n * @example 'categories' -> 'category'\n */\nexport function singularize(name: string): string {\n if (!name) return '';\n if (name.endsWith('ies')) {\n return name.slice(0, -3) + 'y';\n }\n if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) {\n return name.slice(0, -2);\n }\n if (name.endsWith('s') && !name.endsWith('ss')) {\n return name.slice(0, -1);\n }\n return name;\n}\n\n/**\n * Simple pluralize function for English words.\n * @example 'user' -> 'users'\n * @example 'category' -> 'categories'\n */\nexport function pluralize(name: string): string {\n if (!name) return '';\n if (name.endsWith('y') && name.length > 1 && !/[aeiou]/.test(name[name.length - 2])) {\n return name.slice(0, -1) + 'ies';\n }\n if (name.endsWith('s') || name.endsWith('x') || name.endsWith('z') || name.endsWith('ch') || name.endsWith('sh')) {\n return name + 'es';\n }\n return name + 's';\n}\n",
9
- "import type { Type, UniversalQuerier } from '../type/index.js';\n// import from the specific util modules (not the barrel) to keep entity\n// metadata and reflect-metadata out of the browser bundle\nimport { getKeys } from '../util/object.util.js';\nimport { kebabCase } from '../util/string.util.js';\n\ntype RouteShape = {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n readonly path: '' | `/${string}`;\n};\n\n/**\n * Single source of truth for the CRUD-over-HTTP surface, shared by server adapters and the browser client.\n * Keys are constrained to {@link UniversalQuerier} method names, so renaming a querier method\n * (or routing a non-existent one) is a compile error.\n */\nexport const CRUD_ROUTES = {\n findMany: { method: 'GET', path: '' },\n findOne: { method: 'GET', path: '/one' },\n count: { method: 'GET', path: '/count' },\n findOneById: { method: 'GET', path: '/:id' },\n insertOne: { method: 'POST', path: '' },\n insertMany: { method: 'POST', path: '/many' },\n saveOne: { method: 'PUT', path: '' },\n saveMany: { method: 'PUT', path: '/many' },\n updateMany: { method: 'PATCH', path: '' },\n updateOneById: { method: 'PATCH', path: '/:id' },\n deleteOneById: { method: 'DELETE', path: '/:id' },\n deleteMany: { method: 'DELETE', path: '' },\n} as const satisfies Partial<Record<keyof UniversalQuerier, RouteShape>>;\n\nexport type CrudOperation = keyof typeof CRUD_ROUTES;\n\nexport type CrudRoute = (typeof CRUD_ROUTES)[CrudOperation];\n\n/**\n * `QUERY` (RFC 10008) is an alternate transport for the read operations: same semantics as the\n * GET routes, but the JSON query travels in the request body instead of the query string,\n * avoiding URL-length limits for large queries.\n */\nexport type HttpMethod = CrudRoute['method'] | 'QUERY';\n\nconst CRUD_OPS = getKeys(CRUD_ROUTES);\n\n// derived from CRUD_ROUTES (the literal-path GET routes) so the sub-paths live in exactly one place\nconst QUERY_READ_OPS: ReadonlyMap<string, CrudOperation> = new Map(\n CRUD_OPS.filter((op) => CRUD_ROUTES[op].method === 'GET' && CRUD_ROUTES[op].path !== '/:id').map((op) => [\n CRUD_ROUTES[op].path,\n op,\n ]),\n);\n\n/**\n * URL segment for an entity, e.g. `entityPath(UserProfile) === 'user-profile'`.\n */\nexport function entityPath<E>(entity: Type<E>): string {\n return kebabCase(entity.name);\n}\n\nexport type RouteMatch = {\n readonly op: CrudOperation;\n /**\n * the resolved transport method - differs from the op's canonical route method for QUERY.\n */\n readonly method: HttpMethod;\n readonly id?: string;\n};\n\n/**\n * Resolve a (method, sub-path) pair to a CRUD operation. Literal sub-paths win over `:id`.\n */\nexport function matchRoute(method: string, subPath: string | undefined): RouteMatch | undefined {\n const raw = method.toUpperCase();\n const literal = subPath === undefined ? '' : `/${subPath}`;\n if (raw === 'QUERY') {\n const op = QUERY_READ_OPS.get(literal);\n return op ? { op, method: 'QUERY' } : undefined;\n }\n // HEAD reads like GET per HTTP semantics; the server runtime omits the response body\n const verb = raw === 'HEAD' ? 'GET' : raw;\n let idOp: CrudOperation | undefined;\n for (const op of CRUD_OPS) {\n const route = CRUD_ROUTES[op];\n if (route.method !== verb) {\n continue;\n }\n if (route.path === literal) {\n return { op, method: route.method };\n }\n if (route.path === '/:id') {\n idOp = op;\n }\n }\n return idOp && subPath !== undefined ? { op: idOp, method: CRUD_ROUTES[idOp].method, id: subPath } : undefined;\n}\n\nexport type RequestSuccessResponse<E> = {\n data: E;\n count?: number;\n};\n\nexport type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {\n count: number;\n};\n\nexport type RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
10
- "import type { Query, QueryOptions } from '../type/index.js';\n// specific util module (not the barrel) to keep reflect-metadata out of the browser bundle\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
9
+ "import type { Type, UniversalQuerier } from '../type/index.js';\n// the specific util modules, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\nimport { kebabCase } from '../util/string.util.js';\n\ntype RouteShape = {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n readonly path: '' | `/${string}`;\n};\n\n/**\n * Single source of truth for the CRUD-over-HTTP surface, shared by server adapters and the browser client.\n * Keys are constrained to {@link UniversalQuerier} method names, so renaming a querier method\n * (or routing a non-existent one) is a compile error.\n */\nexport const CRUD_ROUTES = {\n findMany: { method: 'GET', path: '' },\n findOne: { method: 'GET', path: '/one' },\n count: { method: 'GET', path: '/count' },\n findOneById: { method: 'GET', path: '/:id' },\n insertOne: { method: 'POST', path: '' },\n insertMany: { method: 'POST', path: '/many' },\n saveOne: { method: 'PUT', path: '' },\n saveMany: { method: 'PUT', path: '/many' },\n updateMany: { method: 'PATCH', path: '' },\n updateOneById: { method: 'PATCH', path: '/:id' },\n deleteOneById: { method: 'DELETE', path: '/:id' },\n deleteMany: { method: 'DELETE', path: '' },\n} as const satisfies Partial<Record<keyof UniversalQuerier, RouteShape>>;\n\nexport type CrudOperation = keyof typeof CRUD_ROUTES;\n\nexport type CrudRoute = (typeof CRUD_ROUTES)[CrudOperation];\n\n/**\n * `QUERY` (RFC 10008) is an alternate transport for the read operations: same semantics as the\n * GET routes, but the JSON query travels in the request body instead of the query string,\n * avoiding URL-length limits for large queries.\n */\nexport type HttpMethod = CrudRoute['method'] | 'QUERY';\n\nconst CRUD_OPS = getKeys(CRUD_ROUTES);\n\n// derived from CRUD_ROUTES (the literal-path GET routes) so the sub-paths live in exactly one place\nconst QUERY_READ_OPS: ReadonlyMap<string, CrudOperation> = new Map(\n CRUD_OPS.filter((op) => CRUD_ROUTES[op].method === 'GET' && CRUD_ROUTES[op].path !== '/:id').map((op) => [\n CRUD_ROUTES[op].path,\n op,\n ]),\n);\n\n/**\n * URL segment for an entity, e.g. `entityPath(UserProfile) === 'user-profile'`.\n */\nexport function entityPath<E>(entity: Type<E>): string {\n return kebabCase(entity.name);\n}\n\nexport type RouteMatch = {\n readonly op: CrudOperation;\n /**\n * the resolved transport method - differs from the op's canonical route method for QUERY.\n */\n readonly method: HttpMethod;\n readonly id?: string;\n};\n\n/**\n * Resolve a (method, sub-path) pair to a CRUD operation. Literal sub-paths win over `:id`.\n */\nexport function matchRoute(method: string, subPath: string | undefined): RouteMatch | undefined {\n const raw = method.toUpperCase();\n const literal = subPath === undefined ? '' : `/${subPath}`;\n if (raw === 'QUERY') {\n const op = QUERY_READ_OPS.get(literal);\n return op ? { op, method: 'QUERY' } : undefined;\n }\n // HEAD reads like GET per HTTP semantics; the server runtime omits the response body\n const verb = raw === 'HEAD' ? 'GET' : raw;\n let idOp: CrudOperation | undefined;\n for (const op of CRUD_OPS) {\n const route = CRUD_ROUTES[op];\n if (route.method !== verb) {\n continue;\n }\n if (route.path === literal) {\n return { op, method: route.method };\n }\n if (route.path === '/:id') {\n idOp = op;\n }\n }\n return idOp && subPath !== undefined ? { op: idOp, method: CRUD_ROUTES[idOp].method, id: subPath } : undefined;\n}\n\nexport type RequestSuccessResponse<E> = {\n data: E;\n count?: number;\n};\n\nexport type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {\n count: number;\n};\n\nexport type RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
10
+ "import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
11
11
  "import {\n CRUD_ROUTES,\n entityPath,\n type HttpMethod,\n type RequestCountedSuccessResponse,\n type RequestSuccessResponse,\n} from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type { IdValue, Query, QueryOne, QueryOptions, QuerySearch, Type, UpdatePayload } from '../../type/index.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n};\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n findOneById<E extends object>(\n entity: Type<E>,\n id: IdValue<E>,\n q?: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<E | undefined>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n findOne<E extends object>(\n entity: Type<E>,\n q: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n return this.read<E | undefined>(`${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`, q, opts);\n }\n\n findMany<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<E[]>> {\n const data: Query<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<E[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<E[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.insertMany.path}`, payload, this.buildOptions(opts));\n }\n\n updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${id}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.saveMany.path}`, payload, this.buildOptions(opts));\n }\n\n deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${entityPath(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
12
12
  "import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
13
13
  ],
14
- "mappings": "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",
14
+ "mappings": "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",
15
15
  "debugId": "89C8E712B5A364FC64756E2164756E21",
16
16
  "names": []
17
17
  }
@@ -221,7 +221,20 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
221
221
  */
222
222
  protected appendDefaultInsertValue(ctx: QueryContext, _field: FieldOptions | undefined): void;
223
223
  update<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): void;
224
- upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
224
+ /**
225
+ * `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...`, which SQLite adopted from Postgres
226
+ * and which every dialect here speaks except the MySQL family (see {@link MysqlLikeSqlDialect}).
227
+ *
228
+ * Two orderings matter, and they pull in opposite directions. The assignments are computed *before*
229
+ * the insert, because `appendInsertValues` fills `onInsert` fields into the payload and a column that
230
+ * exists only there - `createdAt` - must not join the update set. Their bound values are pushed
231
+ * *after* it, because a `?` placeholder is positional and the clause comes last in the statement.
232
+ * {@link PgLikeSqlDialect} overrides this: `$N` placeholders make array order irrelevant, so it can
233
+ * bind into the main context and skip the second one.
234
+ */
235
+ upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
236
+ /** How an `ON CONFLICT` assignment reads the row that was being inserted. */
237
+ protected readonly upsertExcluded: (columnName: string) => string;
225
238
  protected getUpsertUpdateAssignments<E>(ctx: QueryContext, meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], callback?: (columnName: string) => string): string;
226
239
  protected getUpsertConflictPathsStr<E>(meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>): string;
227
240
  delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
@@ -1006,22 +1006,29 @@ export class AbstractSqlDialect extends IndexSqlDialect {
1006
1006
  }
1007
1007
  this.search(ctx, entity, q, opts);
1008
1008
  }
1009
- upsert(ctx, entity, conflictPaths, payload) {
1009
+ /**
1010
+ * `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...`, which SQLite adopted from Postgres
1011
+ * and which every dialect here speaks except the MySQL family (see {@link MysqlLikeSqlDialect}).
1012
+ *
1013
+ * Two orderings matter, and they pull in opposite directions. The assignments are computed *before*
1014
+ * the insert, because `appendInsertValues` fills `onInsert` fields into the payload and a column that
1015
+ * exists only there - `createdAt` - must not join the update set. Their bound values are pushed
1016
+ * *after* it, because a `?` placeholder is positional and the clause comes last in the statement.
1017
+ * {@link PgLikeSqlDialect} overrides this: `$N` placeholders make array order irrelevant, so it can
1018
+ * bind into the main context and skip the second one.
1019
+ */
1020
+ upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
1010
1021
  const meta = getMeta(entity);
1011
1022
  const updateCtx = this.createContext();
1012
- const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, (name) => `VALUES(${name})`);
1013
- if (update) {
1014
- this.appendInsertValues(ctx, entity, payload);
1015
- ctx.append(` ON DUPLICATE KEY UPDATE ${update}`);
1016
- ctx.pushValue(...updateCtx.values);
1017
- }
1018
- else {
1019
- const insertCtx = this.createContext();
1020
- this.appendInsertValues(insertCtx, entity, payload);
1021
- ctx.append(insertCtx.sql.replace(/^INSERT/, 'INSERT IGNORE'));
1022
- ctx.pushValue(...insertCtx.values);
1023
- }
1024
- }
1023
+ const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, this.upsertExcluded);
1024
+ const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
1025
+ const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
1026
+ this.appendInsertValues(ctx, entity, payload);
1027
+ ctx.append(` ON CONFLICT (${keys}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
1028
+ ctx.pushValue(...updateCtx.values);
1029
+ }
1030
+ /** How an `ON CONFLICT` assignment reads the row that was being inserted. */
1031
+ upsertExcluded = (columnName) => `EXCLUDED.${columnName}`;
1025
1032
  getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, callback) {
1026
1033
  const sample = Array.isArray(payload) ? payload[0] : payload;
1027
1034
  const cloned = { ...sample };
@@ -1,4 +1,4 @@
1
- import type { DialectFeatures, EntityMeta, FieldOptions, IndexFeature, IndexSchema, InsertIdSource, QueryContext, QuerySizeComparisonOps, QueryTextSearchOptions, Type } from '../type/index.js';
1
+ import type { DialectFeatures, EntityMeta, FieldOptions, IndexFeature, IndexSchema, InsertIdSource, QueryConflictPaths, QueryContext, QuerySizeComparisonOps, QueryTextSearchOptions, Type } from '../type/index.js';
2
2
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
3
  /**
4
4
  * Shared JSON-array / JSON-object operator implementation between MySQL and MariaDB.
@@ -26,6 +26,16 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
26
26
  readonly alterColumnSyntax = "MODIFY COLUMN";
27
27
  readonly booleanLiteral = "integer";
28
28
  readonly insertIdSource: InsertIdSource;
29
+ /**
30
+ * `INSERT ... ON DUPLICATE KEY UPDATE`, and `INSERT IGNORE` when every non-conflict column is itself a
31
+ * conflict key so there is nothing to assign. Neither form takes a conflict target: MySQL picks the
32
+ * unique index for you.
33
+ *
34
+ * The update assignments are built into their own context and pushed afterwards, since they read
35
+ * `VALUES(col)` rather than binding, and any value they *do* bind (an `onUpdate` field absent from the
36
+ * payload) has to land after the insert's for a `?`-placeholder driver.
37
+ */
38
+ upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
29
39
  readonly maxBindValues: number;
30
40
  escape(value: unknown): string;
31
41
  /**
@@ -1,3 +1,4 @@
1
+ import { getMeta } from '../entity/index.js';
1
2
  import { getFieldKeys } from '../util/index.js';
2
3
  import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
3
4
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
@@ -44,6 +45,30 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
44
45
  // No `RETURNING` support, so multi-row insert IDs are inferred from the header - see the
45
46
  // `innodb_autoinc_lock_mode` caveat on `buildUpdateResult` in `util/sql.util.ts`.
46
47
  insertIdSource = 'firstId';
48
+ /**
49
+ * `INSERT ... ON DUPLICATE KEY UPDATE`, and `INSERT IGNORE` when every non-conflict column is itself a
50
+ * conflict key so there is nothing to assign. Neither form takes a conflict target: MySQL picks the
51
+ * unique index for you.
52
+ *
53
+ * The update assignments are built into their own context and pushed afterwards, since they read
54
+ * `VALUES(col)` rather than binding, and any value they *do* bind (an `onUpdate` field absent from the
55
+ * payload) has to land after the insert's for a `?`-placeholder driver.
56
+ */
57
+ upsert(ctx, entity, conflictPaths, payload) {
58
+ const meta = getMeta(entity);
59
+ const updateCtx = this.createContext();
60
+ const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, (name) => `VALUES(${name})`);
61
+ if (update) {
62
+ this.appendInsertValues(ctx, entity, payload);
63
+ ctx.append(` ON DUPLICATE KEY UPDATE ${update}`);
64
+ ctx.pushValue(...updateCtx.values);
65
+ return;
66
+ }
67
+ const insertCtx = this.createContext();
68
+ this.appendInsertValues(insertCtx, entity, payload);
69
+ ctx.append(insertCtx.sql.replace(/^INSERT/, 'INSERT IGNORE'));
70
+ ctx.pushValue(...insertCtx.values);
71
+ }
47
72
  maxBindValues = 65535;
48
73
  escape(value) {
49
74
  return escapeMysqlSqlLiteral(value);
@@ -46,15 +46,12 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
46
46
  protected indexInclude(index: IndexSchema): string;
47
47
  protected indexTuning(index: IndexSchema): string;
48
48
  placeholder(index: number): string;
49
- upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
50
49
  /**
51
- * Shared `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...` builder, assembling
52
- * everything upfront rather than handing back fragments, for the `$N`-placeholder Postgres-wire
53
- * dialects. `extraReturning` lets {@link PostgresDialect} append `(xmax = 0) AS "_created"` to
54
- * detect insert-vs-update; CockroachDB has no `xmax`/`ctid` system columns, so it uses the default
55
- * (empty) and `created` stays `undefined` in the result.
50
+ * The same statement as the base, binding the assignments into the main context instead of a second
51
+ * one: `$N` placeholders carry their own index, so the values need not be in statement order, and
52
+ * computing them first is what keeps `appendInsertValues`' `onInsert` fields out of the update set.
56
53
  */
57
- protected buildUpsertOnConflict<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
54
+ upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
58
55
  /**
59
56
  * `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
60
57
  * (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `to_tsquery`, which
@@ -115,23 +115,18 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
115
115
  placeholder(index) {
116
116
  return `$${index}`;
117
117
  }
118
- upsert(ctx, entity, conflictPaths, payload) {
119
- this.buildUpsertOnConflict(ctx, entity, conflictPaths, payload);
120
- }
121
118
  /**
122
- * Shared `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...` builder, assembling
123
- * everything upfront rather than handing back fragments, for the `$N`-placeholder Postgres-wire
124
- * dialects. `extraReturning` lets {@link PostgresDialect} append `(xmax = 0) AS "_created"` to
125
- * detect insert-vs-update; CockroachDB has no `xmax`/`ctid` system columns, so it uses the default
126
- * (empty) and `created` stays `undefined` in the result.
119
+ * The same statement as the base, binding the assignments into the main context instead of a second
120
+ * one: `$N` placeholders carry their own index, so the values need not be in statement order, and
121
+ * computing them first is what keeps `appendInsertValues`' `onInsert` fields out of the update set.
127
122
  */
128
- buildUpsertOnConflict(ctx, entity, conflictPaths, payload, extraReturning = '') {
123
+ upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
129
124
  const meta = getMeta(entity);
130
- const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, (name) => `EXCLUDED.${name}`);
131
- const keysStr = this.getUpsertConflictPathsStr(meta, conflictPaths);
125
+ const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, this.upsertExcluded);
126
+ const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
132
127
  const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
133
128
  this.appendInsertValues(ctx, entity, payload);
134
- ctx.append(` ON CONFLICT (${keysStr}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
129
+ ctx.append(` ON CONFLICT (${keys}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
135
130
  }
136
131
  /**
137
132
  * `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
@@ -0,0 +1,35 @@
1
+ import type { FieldOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
2
+ /**
3
+ * What the member decorators record for one class, waiting for `@Entity()` or `defineEntity` to drain
4
+ * it into the metadata registry. Member decorators receive no class reference under the standard
5
+ * decorator spec, so this object is the only channel between them and the class decorator that does.
6
+ */
7
+ export type MemberRegistrations = {
8
+ readonly fields: Record<string, FieldOptions>;
9
+ readonly relations: Record<string, RelationOptions>;
10
+ readonly hooks: Partial<Record<HookEvent, string[]>>;
11
+ };
12
+ /**
13
+ * The calling class's own registrations, created on first use.
14
+ *
15
+ * Deliberately holds **only** this class's members: inheritance is resolved later by walking the class
16
+ * prototype chain, not by reading through the metadata object's. tsc chains a subclass's metadata to its
17
+ * parent's and SWC does not, so anything built on that chain would work under one compiler and quietly
18
+ * lose inherited fields under the other. Keeping each bag to its own members also means a parent's map
19
+ * is never shared with its subclasses, and hooks cannot be registered twice.
20
+ */
21
+ export declare function memberRegistrations(metadata: DecoratorMetadata): MemberRegistrations;
22
+ /**
23
+ * Takes the registrations belonging to `metadata`, leaving none behind, so finalizing an entity twice
24
+ * cannot register its hooks twice.
25
+ */
26
+ export declare function drainRegistrations(metadata: DecoratorMetadata | undefined): MemberRegistrations | undefined;
27
+ /**
28
+ * The registrations a class made for itself.
29
+ *
30
+ * @remarks Only usable once the class is fully defined, which is why `@Entity()` reads
31
+ * `context.metadata` instead: TypeScript attaches `Symbol.metadata` to the class *after* its class
32
+ * decorators return. Ancestors are always fully defined by then, so this is how inherited members are
33
+ * collected.
34
+ */
35
+ export declare function ownRegistrations(entity: Type<unknown>): MemberRegistrations | undefined;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Polyfill `Symbol.metadata`, which no runtime we support defines yet (checked on Node 24 and Bun
3
+ * 1.3): TypeScript's decorator emit reads it to decide whether to build the metadata object at all, so
4
+ * without this every `context.metadata` is `undefined` and field registration is silently dropped
5
+ * rather than failing.
6
+ *
7
+ * `Symbol.for`, not `Symbol()`, so a duplicated copy of this module (HMR, federated bundles, ESM+CJS
8
+ * dual-loading) lands on the same symbol, and so it agrees with the key esbuild and SWC fall back to
9
+ * (`Symbol.metadata ?? Symbol.for('Symbol.metadata')`). Assigned through a widened alias because the
10
+ * lib declares the property `readonly`; when a runtime does define it, `??=` leaves it alone.
11
+ */
12
+ const symbolCtor = Symbol;
13
+ symbolCtor.metadata ??= Symbol.for('Symbol.metadata');
14
+ /** Where member registrations live on the per-class metadata object. */
15
+ const registrations = Symbol.for('uql-orm/entity/decoratorMembers');
16
+ /**
17
+ * The calling class's own registrations, created on first use.
18
+ *
19
+ * Deliberately holds **only** this class's members: inheritance is resolved later by walking the class
20
+ * prototype chain, not by reading through the metadata object's. tsc chains a subclass's metadata to its
21
+ * parent's and SWC does not, so anything built on that chain would work under one compiler and quietly
22
+ * lose inherited fields under the other. Keeping each bag to its own members also means a parent's map
23
+ * is never shared with its subclasses, and hooks cannot be registered twice.
24
+ */
25
+ export function memberRegistrations(metadata) {
26
+ if (!Object.hasOwn(metadata, registrations)) {
27
+ metadata[registrations] = { fields: {}, relations: {}, hooks: {} };
28
+ }
29
+ return metadata[registrations];
30
+ }
31
+ /**
32
+ * Takes the registrations belonging to `metadata`, leaving none behind, so finalizing an entity twice
33
+ * cannot register its hooks twice.
34
+ */
35
+ export function drainRegistrations(metadata) {
36
+ if (!metadata || !Object.hasOwn(metadata, registrations)) {
37
+ return undefined;
38
+ }
39
+ const own = metadata[registrations];
40
+ delete metadata[registrations];
41
+ return own;
42
+ }
43
+ /**
44
+ * The registrations a class made for itself.
45
+ *
46
+ * @remarks Only usable once the class is fully defined, which is why `@Entity()` reads
47
+ * `context.metadata` instead: TypeScript attaches `Symbol.metadata` to the class *after* its class
48
+ * decorators return. Ancestors are always fully defined by then, so this is how inherited members are
49
+ * collected.
50
+ */
51
+ export function ownRegistrations(entity) {
52
+ const metadata = Object.getOwnPropertyDescriptor(entity, Symbol.metadata)?.value;
53
+ return drainRegistrations(metadata);
54
+ }
@@ -1,2 +1,28 @@
1
- import type { EntityOptions, Type } from '../../type/index.js';
2
- export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>) => void;
1
+ import type { EntityOptions, FieldKey, FilterOptions, IndexColumnInput, IndexOptions, Type } from '../../type/index.js';
2
+ /**
3
+ * Marks a class as an entity and finalizes its metadata.
4
+ *
5
+ * @remarks Takes the registrations from `context.metadata` rather than from the class. Member
6
+ * decorators have already run by the time a class decorator does, but TypeScript defines
7
+ * `Symbol.metadata` on the class *after* the class decorators return, so reading `entity[Symbol.metadata]`
8
+ * here would find only what the base class left behind. `defineEntity` reads it off the class instead,
9
+ * which is correct for the imperative path because it runs later still.
10
+ */
11
+ export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>, context?: ClassDecoratorContext) => void;
12
+ /**
13
+ * Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
14
+ *
15
+ * @example `@Filter('active', { condition: { status: 'active' }, default: false })`
16
+ */
17
+ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity: Type<E>) => void;
18
+ /**
19
+ * Declares a composite index. Stacks, so several may sit above one class.
20
+ *
21
+ * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
22
+ * names be checked against it: `@Index(['nope'])` does not compile.
23
+ *
24
+ * @example `@Index(['lastName', 'firstName'], { name: 'idx_users_fullname' })`
25
+ * @example `@Index(['email'], { unique: true })`
26
+ * @example `@Index(['status'], { where: "status = 'active'" })`
27
+ */
28
+ export declare function Index<E>(columns: readonly IndexColumnInput<FieldKey<E>>[], options?: IndexOptions): (entity: Type<E>) => void;
@@ -1,6 +1,44 @@
1
- import { defineEntity } from '../metadata/definition.js';
1
+ import { applyMembers, defineEntity, defineFilter, defineIndex } from '../metadata/definition.js';
2
+ import { drainRegistrations } from './bag.js';
3
+ // The class-level decorators. Unlike the member ones they receive the class, so each is a direct call
4
+ // into the registry with no bag in between.
5
+ /**
6
+ * Marks a class as an entity and finalizes its metadata.
7
+ *
8
+ * @remarks Takes the registrations from `context.metadata` rather than from the class. Member
9
+ * decorators have already run by the time a class decorator does, but TypeScript defines
10
+ * `Symbol.metadata` on the class *after* the class decorators return, so reading `entity[Symbol.metadata]`
11
+ * here would find only what the base class left behind. `defineEntity` reads it off the class instead,
12
+ * which is correct for the imperative path because it runs later still.
13
+ */
2
14
  export function Entity(opts) {
3
- return (entity) => {
15
+ return (entity, context) => {
16
+ applyMembers(entity, drainRegistrations(context?.metadata));
4
17
  defineEntity(entity, opts);
5
18
  };
6
19
  }
20
+ /**
21
+ * Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
22
+ *
23
+ * @example `@Filter('active', { condition: { status: 'active' }, default: false })`
24
+ */
25
+ export function Filter(name, opts) {
26
+ return (entity) => {
27
+ defineFilter(entity, name, opts);
28
+ };
29
+ }
30
+ /**
31
+ * Declares a composite index. Stacks, so several may sit above one class.
32
+ *
33
+ * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
34
+ * names be checked against it: `@Index(['nope'])` does not compile.
35
+ *
36
+ * @example `@Index(['lastName', 'firstName'], { name: 'idx_users_fullname' })`
37
+ * @example `@Index(['email'], { unique: true })`
38
+ * @example `@Index(['status'], { where: "status = 'active'" })`
39
+ */
40
+ export function Index(columns, options = {}) {
41
+ return (entity) => {
42
+ defineIndex(entity, { ...options, columns });
43
+ };
44
+ }
@@ -0,0 +1,51 @@
1
+ import type { EntityGetter, FieldOptions, FieldType, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
2
+ /** A member decorator that also constrains the property it may be applied to. */
3
+ type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<unknown, V>) => void;
4
+ /**
5
+ * Declares a persisted field.
6
+ *
7
+ * `@Field({ type: String })` on a `number` property is a compile error rather than a silent TEXT column,
8
+ * which is what makes the now-mandatory `type` worth stating.
9
+ *
10
+ * @example `@Field({ type: String }) name?: string;`
11
+ * @example `@Field({ references: () => Company }) companyId?: string;`
12
+ */
13
+ export declare function Field<T extends FieldType>(opts: FieldOptions & {
14
+ readonly type: T;
15
+ }): MemberDecorator<TsTypeOf<T> | undefined>;
16
+ /**
17
+ * A foreign key may omit `type`: schema generation resolves it from the referenced primary key, so the
18
+ * column picks up that key's `columnType`, length and chained references rather than a guess.
19
+ */
20
+ export declare function Field(opts: FieldOptions & {
21
+ readonly references: EntityGetter;
22
+ }): MemberDecorator<unknown>;
23
+ /**
24
+ * Declares the primary key, checked the same way as `@Field`.
25
+ *
26
+ * @example `@Id({ type: Number }) id?: number;`
27
+ * @example `@Id({ type: 'uuid', onInsert: uuidv7 }) id?: string;`
28
+ */
29
+ export declare function Id<T extends FieldType>(opts: FieldOptions & {
30
+ readonly type: T;
31
+ }): MemberDecorator<TsTypeOf<T> | undefined>;
32
+ /**
33
+ * `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
34
+ * entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
35
+ * cardinality on a non-array property does too. `entity` is required because nothing reflects it now.
36
+ */
37
+ type WithEntity<E, O> = O & {
38
+ readonly entity: EntityGetter<E>;
39
+ };
40
+ export declare function OneToOne<E>(opts: WithEntity<E, RelationOneToOneOptions<E>>): MemberDecorator<E | undefined>;
41
+ export declare function ManyToOne<E>(opts: WithEntity<E, RelationManyToOneOptions<E>>): MemberDecorator<E | undefined>;
42
+ export declare function OneToMany<E>(opts: WithEntity<E, RelationOneToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
43
+ export declare function ManyToMany<E>(opts: WithEntity<E, RelationManyToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
44
+ export declare const BeforeInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
45
+ export declare const AfterInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
46
+ export declare const BeforeUpdate: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
47
+ export declare const AfterUpdate: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
48
+ export declare const BeforeDelete: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
49
+ export declare const AfterDelete: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
50
+ export declare const AfterLoad: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
51
+ export {};
@@ -0,0 +1,51 @@
1
+ import { memberRegistrations } from './bag.js';
2
+ export function Field(opts) {
3
+ return (_value, context) => {
4
+ memberRegistrations(context.metadata).fields[String(context.name)] = opts;
5
+ };
6
+ }
7
+ /**
8
+ * Declares the primary key, checked the same way as `@Field`.
9
+ *
10
+ * @example `@Id({ type: Number }) id?: number;`
11
+ * @example `@Id({ type: 'uuid', onInsert: uuidv7 }) id?: string;`
12
+ */
13
+ export function Id(opts) {
14
+ return (_value, context) => {
15
+ memberRegistrations(context.metadata).fields[String(context.name)] = { ...opts, isId: true };
16
+ };
17
+ }
18
+ function relation(opts) {
19
+ return (_value, context) => {
20
+ memberRegistrations(context.metadata).relations[String(context.name)] = opts;
21
+ };
22
+ }
23
+ export function OneToOne(opts) {
24
+ return relation({ cardinality: '11', ...opts });
25
+ }
26
+ export function ManyToOne(opts) {
27
+ return relation({ cardinality: 'm1', ...opts });
28
+ }
29
+ export function OneToMany(opts) {
30
+ return relation({ cardinality: '1m', ...opts });
31
+ }
32
+ export function ManyToMany(opts) {
33
+ return relation({ cardinality: 'mm', ...opts });
34
+ }
35
+ function hook(event) {
36
+ return () =>
37
+ // Generic in `This` so a hook declared with an explicit `this` parameter still matches; the default
38
+ // on `ClassMethodDecoratorContext` pins `this` to `unknown` and would reject it.
39
+ (_value, context) => {
40
+ const { hooks } = memberRegistrations(context.metadata);
41
+ hooks[event] ??= [];
42
+ hooks[event].push(String(context.name));
43
+ };
44
+ }
45
+ export const BeforeInsert = hook('beforeInsert');
46
+ export const AfterInsert = hook('afterInsert');
47
+ export const BeforeUpdate = hook('beforeUpdate');
48
+ export const AfterUpdate = hook('afterUpdate');
49
+ export const BeforeDelete = hook('beforeDelete');
50
+ export const AfterDelete = hook('afterDelete');
51
+ export const AfterLoad = hook('afterLoad');
@@ -1 +1,3 @@
1
- export * from './decorator/index.js';
1
+ export * from './decorator/entity.js';
2
+ export * from './decorator/members.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, } from './metadata/definition.js';
@@ -1 +1,3 @@
1
- export * from './decorator/index.js';
1
+ export * from './decorator/entity.js';
2
+ export * from './decorator/members.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, } from './metadata/definition.js';
@@ -1,17 +1,31 @@
1
- import type { EntityIndexInput, EntityMeta, EntityOptions, FieldOptions, FilterOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
2
- /**
3
- * Append a composite index entry with normalized `unique` (default false) and columns. Normalizing
4
- * here is what lets the dialects render one shape instead of re-parsing the authored sugar.
5
- */
6
- export declare function appendEntityIndex<E>(meta: EntityMeta<E>, index: EntityIndexInput): void;
1
+ import type { EntityIndexInput, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
7
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
8
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
9
- export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions<E>): EntityMeta<E>;
4
+ export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
10
5
  export declare function defineHook<E>(entity: Type<E>, methodName: string, event: HookEvent): EntityMeta<E>;
11
- export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput): EntityMeta<E>;
6
+ /**
7
+ * Declares a composite index. `unique` and the authored column sugar are normalized here, which is what
8
+ * lets the dialects render one shape instead of re-parsing it.
9
+ */
10
+ export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E>>): EntityMeta<E>;
12
11
  export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
12
+ /**
13
+ * What a decorator bag and {@link EntityOptions} have in common at registration time. The keyed mapped
14
+ * types in `EntityOptions<E>` are what check the imperative call; a member decorator has no class to key
15
+ * against, so by the time either reaches the primitives the keys are plain strings.
16
+ */
17
+ type MemberSpecs = {
18
+ readonly fields?: Readonly<Record<string, FieldOptions | undefined>>;
19
+ readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
20
+ readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
21
+ };
22
+ /**
23
+ * Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
24
+ * API converge on one registration path before anything is finalized.
25
+ */
26
+ export declare function applyMembers<E>(entity: Type<E>, specs: MemberSpecs | undefined): void;
13
27
  export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
14
28
  export declare function getEntities(): Type<unknown>[];
15
29
  export declare function ensureMeta<E>(entity: Type<E>): EntityMeta<E>;
16
30
  export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
17
- export declare function isValidEntityType(type: unknown): type is Type<unknown>;
31
+ export {};