uql-orm 0.39.0 → 0.40.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.
- package/README.md +0 -8
- package/dist/browser/uql-browser.min.js.map +2 -2
- package/dist/bunSql/bunSqlQuerierPool.js +1 -1
- package/dist/dialect/abstractSqlDialect.js +9 -18
- package/dist/entity/metadata/definition.js +8 -3
- package/dist/http/handler.js +1 -1
- package/dist/migrate/builder/migrationBuilder.js +2 -1
- package/dist/migrate/builder/tableBuilder.js +2 -1
- package/dist/migrate/codegen/indexDecoratorSource.js +9 -9
- package/dist/migrate/drift/index.d.ts +1 -1
- package/dist/migrate/drift/index.js +1 -1
- package/dist/mongo/mongodbQuerier.js +1 -1
- package/dist/querier/abstractSqlQuerier.js +1 -1
- package/dist/schema/dependencyGraph.d.ts +18 -0
- package/dist/schema/dependencyGraph.js +62 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/schemaAST.d.ts +0 -5
- package/dist/schema/schemaAST.js +4 -49
- package/dist/type/entity.d.ts +8 -3
- package/dist/type/migration.d.ts +1 -1
- package/dist/type/query.d.ts +2 -2
- package/dist/type/queryRaw.d.ts +12 -0
- package/dist/type/queryRaw.js +24 -0
- package/dist/util/indexColumn.util.d.ts +3 -1
- package/dist/util/indexColumn.util.js +8 -4
- package/dist/util/raw.d.ts +37 -11
- package/dist/util/raw.js +36 -14
- package/dist/util/sqlLiteral.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,14 +68,6 @@ The query is just JSON: build it dynamically, store it, diff it, or send it from
|
|
|
68
68
|
|
|
69
69
|
Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/main/CHANGELOG.md).
|
|
70
70
|
|
|
71
|
-
## Made with UQL
|
|
72
|
-
|
|
73
|
-
**[Variability.ai](https://variability.ai)** - AI meeting notetaker and video summarizer for Zoom, Meet, Slack, and Teams. Instant summaries with action items in 35+ languages. Built by UQL's author.
|
|
74
|
-
|
|
75
|
-
Built something? [Open a PR](https://github.com/rogerpadilla/uql/blob/main/CONTRIBUTING.md) and add it here.
|
|
76
|
-
|
|
77
|
-
[](https://uql-orm.dev)
|
|
78
|
-
|
|
79
71
|
---
|
|
80
72
|
|
|
81
73
|
## ⭐ Like what we're doing? Give us a star
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import type { RequestCallback, RequestNotification } from '../type/index.js';\n\nconst subscriptors: RequestCallback[] = [];\n\nexport function notify(notification: RequestNotification): void {\n for (const subscriptor of subscriptors) {\n subscriptor(notification);\n }\n}\n\nexport function on(cb: RequestCallback): () => void {\n subscriptors.push(cb);\n const index = subscriptors.length - 1;\n return (): void => {\n subscriptors.splice(index, 1);\n };\n}\n",
|
|
6
6
|
"import type { RequestErrorResponse } from '../../http/contract.js';\nimport type { RequestSuccessResponse } from '../../type/index.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
|
-
"import type { EntityMeta, 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/**\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<T extends object>(obj: T, pred: (key: keyof T & 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\n/**\n * The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets\n * one, so the fallback is for a meta a decorator is still building - which is why the sites spelling\n * this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\nexport function getFieldKeys<E>(
|
|
7
|
+
"import type { EntityMeta, 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/**\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<T extends object>(obj: T, pred: (key: keyof T & 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\n/**\n * The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets\n * one, so the fallback is for a meta a decorator is still building - which is why the sites spelling\n * this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\nexport function getFieldKeys<E>(fields: {\n [K in FieldKey<E>]?: FieldOptions;\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) => {\n // Lower-casing the rest is only right for a word that carries no case of its own: it turns\n // `USER_ID` into `UserId`, but it also turns `tenantId` into `Tenantid`.\n const rest = word === word.toUpperCase() ? word.slice(1).toLowerCase() : word.slice(1);\n return word.charAt(0).toUpperCase() + rest;\n })\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
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 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 { FieldKey, IdKey, JsonFieldPaths, RelationKey, RelationTarget } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\nexport type QuerySelectOptions = {\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically add the prefix for the alias.\n */\n autoPrefixAlias?: boolean;\n};\n\n/**\n * Query field selection - `{ name: true }` whitelists specific fields. Fields only: a relation is a\n * sub-query rather than a projection flag, and a whitelist naming one could not say whether the\n * scalars come with it. Relations go in `$populate`.\n */\nexport type QuerySelect<E> = {\n [K in FieldKey<E>]?: BooleanLike;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. `[raw('*'), raw('LOG10(points)', 'score')]`). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E> = {\n [K in RelationKey<E>]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;\n};\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count. One statement per relation named here, batched over every parent at once, so it\n * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of\n * a relation of the same name that `$populate` filled with rows.\n */\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\nexport type QueryCount<E> = {\n [K in ToManyRelationKey<E>]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = {\n [K in FieldKey<E>]?: true;\n};\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V> = (IsMany<V> extends true\n ? // `$lock` is statement-level, so it is excluded here rather than being silently ignored per\n // relation. `QueryUnique` is a `Pick` and already leaves it out.\n Except<Query<RelationTarget<V>>, '$lock'>\n : QueryUnique<RelationTarget<V>>) & {\n $required?: boolean;\n};\n\n/**\n * Ambient per-request context (e.g. `{ tenantId, userId, roles }`) resolved by parameterized\n * filters. Set with `withContext(ctx, cb)`. It's an `interface` (not a type alias) so you can type\n * your keys once via declaration merging and get them typed wherever context is read:\n *\n * ```ts\n * declare module 'uql-orm' {\n * interface UqlContext { tenantId: number; userId: string }\n * }\n * ```\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterCondition<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly condition: FilterCondition<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it.\n */\n readonly security?: boolean;\n /** What to do when the condition returns `undefined`. Defaults to `skip`, or `throw` for `security`. */\n readonly onMissing?: FilterOnMissing;\n};\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order\n * it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.\n */\ntype ToOneRelationKey<E> = { [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K }[RelationKey<E>];\n\n/** The relation names a parent holds many rows of, which a populated query fills with a list. */\ntype ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;\n\n/**\n * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,\n * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on\n * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:\n * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank\n * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.\n *\n * One mapped type over the three key sets rather than three intersected. The sets are disjoint - a\n * JSON path is dotted, and a field key cannot also be a relation key - and an assignability check\n * against an intersection is repeated per constituent, which made this the single most expensive\n * type in the package to check.\n */\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\nexport type QuerySortMap<E, Vector extends boolean = true> = {\n [K in FieldKey<E> | JsonFieldPaths<E> | RelationKey<E>]?: K extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its own size.\n IsMany<E[K]> extends true\n ? QuerySortByCount\n : QuerySortMap<RelationTarget<E[K]>, false>\n : K extends FieldKey<E>\n ? Vector extends true\n ? NonNullable<E[K]> extends readonly number[]\n ? QuerySortValue\n : QuerySortDirection\n : QuerySortDirection\n : QuerySortDirection;\n};\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E> = QueryFilter<E> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E> = QueryPage<E> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (`[raw('LOG10(points)', 'score')]`, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * take a row-level lock on the rows this query returns (`SELECT ... FOR UPDATE`). Needs an open\n * transaction: outside one the statement commits and drops the lock before the caller can act on\n * the rows, so it is rejected rather than emitted. Locks only the queried entity, never anything\n * reached through `$populate`. SQL only; MongoDB and the SQLite family reject it.\n *\n * Declared here rather than on {@link QuerySearch}, which `update`/`delete` take: that placement\n * is what keeps the clause off those statements at the type level.\n */\n $lock?: QueryLock;\n\n /**\n * how many candidates an approximate-nearest-neighbour index explores before ranking, for a vector\n * search. Higher trades speed for recall; the default is whatever the engine's own is, which is\n * tuned for speed. Ignored where the search is exact (SQLite, libSQL and Turso scan every row) and\n * where the field carries no ANN index, since there is nothing to widen.\n *\n * The units are the index's, not UQL's, so the number is not comparable across index types: it\n * becomes `hnsw.ef_search` or `ivfflat.probes` on Postgres, `mhnsw_ef_search` on MariaDB, and\n * `numCandidates` on MongoDB Atlas. On Postgres it needs an open transaction, since a `SET LOCAL`\n * outside one applies to nothing.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and\n * a validator checking a relation's own query both need, and what each used to enumerate for itself.\n * Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build\n * rather than the runtime if a clause is ever renamed.\n *\n * `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's\n * query accepts, so leaving it out is what excludes it from both.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes, never a relation's own query - the mirror of\n * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a\n * populated relation's rows are assembled after that, so there is nothing for a nested one to count.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E> = Except<Query<E>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that decide a row's shape, captured from the query as written: the field names\n * `$select` and `$exclude` list, the value those maps carry (a falsy one subtracts instead of\n * selecting, as it does at runtime, and a widened map is how a projection that is not statically\n * known announces itself), and the relation names `$populate` lists.\n *\n * Each is captured as a *key set* rather than as the map itself, which is what keeps the checks\n * intact: TypeScript skips excess-property checking on a naked type parameter, so a captured map\n * would take a typo'd key without a word, while a captured key set makes that typo fail its own\n * `FieldKey<E>` / `RelationKey<E>` constraint. Every other clause - `$where`, `$sort`, and each\n * populated relation's own query - stays the concrete {@link Query} it is today.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = QueryStreamProjection<E, S, V, X, P> & {\n // Intersecting the captured names with {@link QueryCount}'s own leaves a to-one relation no key\n // here at all, so counting one is an excess property rather than a value to check. Narrowing the\n // key rather than the value also instantiates `QueryCount<E>` once instead of once per counted\n // relation, worth ~87k instantiations in a consuming project.\n $count?: { [K in C & keyof QueryCount<E>]?: QueryCount<E>[K] };\n};\n\n/**\n * {@link QueryProjection} without `$count`, which a stream cannot honor. Split out rather than\n * subtracted afterwards: an optional key is a *known* key even when its value maps over `never`, so\n * a statement that must not take the clause has to be built without it in the first place.\n * @internal\n */\ntype QueryStreamProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = {\n $select?: { [K in S]?: V } | readonly QueryRaw[];\n $exclude?: { [K in X]?: V };\n $populate?: { [K in P]?: QueryPopulate<E>[K] };\n};\n\n/**\n * A {@link QueryProjected} a stream can honor: no `$count`, which is batched over a result set a\n * stream never holds all of.\n */\nexport type QueryStreamProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = Except<\n Query<E>,\n '$count'\n> &\n QueryStreamProjection<E, S, V, X, P>;\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n> = Query<E> & QueryProjection<E, S, V, X, P, C>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;\n\n/**\n * The keys a query comes back with, mirroring what the runtime projects: the fields a positive\n * `$select` names, or every field minus what a falsy `$select` entry or a truthy `$exclude` entry\n * subtracts, plus the relations `$populate` asked for. A positive `$select` wins outright, which is\n * why `$exclude` is only read on the branch where there is none.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P, C> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P\n // Populating a relation keeps the id whatever the projection says, and so does counting one: both\n // assemble their result by it (`selectFields` puts it back, as does MongoDB's `pipelineProjection`).\n | ([P | C] extends [never] ? never : NamedIdKey<E>);\n\n/**\n * The id key when it can be named, and nothing when it cannot: {@link IdKey} widens to *every* field\n * for an entity whose id is neither branded nor called `id`/`_id`/`uuid`, and adding that back would\n * hand the caller a row claiming fields the query never fetched. Missing an id costs a `$select`\n * entry; promising absent fields is the bug this type exists to prevent.\n * @internal\n */\ntype NamedIdKey<E> = [FieldKey<E>] extends [IdKey<E>] ? never : IdKey<E>;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A row of a find result: the entity narrowed to the fields the query projected, plus the relations\n * it populated - reading anything the query left out is a compile error rather than a silent\n * `undefined`. Modifiers are preserved, so an optional field stays optional. Name a projected row\n * with it where a helper has to take one: `QueryFindResult<User, 'id' | 'name'>`.\n *\n * The entity itself when the query projects nothing, when it uses a raw-projection array (columns,\n * not fields), and when the projection is not uniform - a `Query<E>` built elsewhere, or a map\n * mixing selected and subtracted entries, whose positive keys inference cannot recover. Relations\n * keep their declared type: narrowing them means capturing their queries as maps, which costs those\n * queries their own checks.\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? { [K in keyof E as K extends ProjectedKeys<E, S, V, X, P, C> ? K : never]: E[K] }\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n {\n [K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> ? K : never]: E[K];\n } & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/**\n * result of an update operation.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the inserted IDs, in insertion order. Exact on `'returning'` dialects; inferred from the\n * driver header on the others (see {@link InsertIdSource}), and empty when the header\n * reports no generated ID.\n */\n ids?: PrimaryKey[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
|
|
10
|
+
"import type { FieldKey, IdKey, JsonFieldPaths, RelationKey, RelationTarget } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\nexport type QuerySelectOptions = {\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically add the prefix for the alias.\n */\n autoPrefixAlias?: boolean;\n};\n\n/**\n * Query field selection - `{ name: true }` whitelists specific fields. Fields only: a relation is a\n * sub-query rather than a projection flag, and a whitelist naming one could not say whether the\n * scalars come with it. Relations go in `$populate`.\n */\nexport type QuerySelect<E> = {\n [K in FieldKey<E>]?: BooleanLike;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E> = {\n [K in RelationKey<E>]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;\n};\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count. One statement per relation named here, batched over every parent at once, so it\n * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of\n * a relation of the same name that `$populate` filled with rows.\n */\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\nexport type QueryCount<E> = {\n [K in ToManyRelationKey<E>]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = {\n [K in FieldKey<E>]?: true;\n};\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V> = (IsMany<V> extends true\n ? // `$lock` is statement-level, so it is excluded here rather than being silently ignored per\n // relation. `QueryUnique` is a `Pick` and already leaves it out.\n Except<Query<RelationTarget<V>>, '$lock'>\n : QueryUnique<RelationTarget<V>>) & {\n $required?: boolean;\n};\n\n/**\n * Ambient per-request context (e.g. `{ tenantId, userId, roles }`) resolved by parameterized\n * filters. Set with `withContext(ctx, cb)`. It's an `interface` (not a type alias) so you can type\n * your keys once via declaration merging and get them typed wherever context is read:\n *\n * ```ts\n * declare module 'uql-orm' {\n * interface UqlContext { tenantId: number; userId: string }\n * }\n * ```\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterCondition<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly condition: FilterCondition<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it.\n */\n readonly security?: boolean;\n /** What to do when the condition returns `undefined`. Defaults to `skip`, or `throw` for `security`. */\n readonly onMissing?: FilterOnMissing;\n};\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order\n * it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.\n */\ntype ToOneRelationKey<E> = { [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K }[RelationKey<E>];\n\n/** The relation names a parent holds many rows of, which a populated query fills with a list. */\ntype ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;\n\n/**\n * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,\n * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on\n * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:\n * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank\n * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.\n *\n * One mapped type over the three key sets rather than three intersected. The sets are disjoint - a\n * JSON path is dotted, and a field key cannot also be a relation key - and an assignability check\n * against an intersection is repeated per constituent, which made this the single most expensive\n * type in the package to check.\n */\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\nexport type QuerySortMap<E, Vector extends boolean = true> = {\n [K in FieldKey<E> | JsonFieldPaths<E> | RelationKey<E>]?: K extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its own size.\n IsMany<E[K]> extends true\n ? QuerySortByCount\n : QuerySortMap<RelationTarget<E[K]>, false>\n : K extends FieldKey<E>\n ? Vector extends true\n ? NonNullable<E[K]> extends readonly number[]\n ? QuerySortValue\n : QuerySortDirection\n : QuerySortDirection\n : QuerySortDirection;\n};\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E> = QueryFilter<E> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E> = QueryPage<E> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * take a row-level lock on the rows this query returns (`SELECT ... FOR UPDATE`). Needs an open\n * transaction: outside one the statement commits and drops the lock before the caller can act on\n * the rows, so it is rejected rather than emitted. Locks only the queried entity, never anything\n * reached through `$populate`. SQL only; MongoDB and the SQLite family reject it.\n *\n * Declared here rather than on {@link QuerySearch}, which `update`/`delete` take: that placement\n * is what keeps the clause off those statements at the type level.\n */\n $lock?: QueryLock;\n\n /**\n * how many candidates an approximate-nearest-neighbour index explores before ranking, for a vector\n * search. Higher trades speed for recall; the default is whatever the engine's own is, which is\n * tuned for speed. Ignored where the search is exact (SQLite, libSQL and Turso scan every row) and\n * where the field carries no ANN index, since there is nothing to widen.\n *\n * The units are the index's, not UQL's, so the number is not comparable across index types: it\n * becomes `hnsw.ef_search` or `ivfflat.probes` on Postgres, `mhnsw_ef_search` on MariaDB, and\n * `numCandidates` on MongoDB Atlas. On Postgres it needs an open transaction, since a `SET LOCAL`\n * outside one applies to nothing.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and\n * a validator checking a relation's own query both need, and what each used to enumerate for itself.\n * Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build\n * rather than the runtime if a clause is ever renamed.\n *\n * `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's\n * query accepts, so leaving it out is what excludes it from both.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes, never a relation's own query - the mirror of\n * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a\n * populated relation's rows are assembled after that, so there is nothing for a nested one to count.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E> = Except<Query<E>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that decide a row's shape, captured from the query as written: the field names\n * `$select` and `$exclude` list, the value those maps carry (a falsy one subtracts instead of\n * selecting, as it does at runtime, and a widened map is how a projection that is not statically\n * known announces itself), and the relation names `$populate` lists.\n *\n * Each is captured as a *key set* rather than as the map itself, which is what keeps the checks\n * intact: TypeScript skips excess-property checking on a naked type parameter, so a captured map\n * would take a typo'd key without a word, while a captured key set makes that typo fail its own\n * `FieldKey<E>` / `RelationKey<E>` constraint. Every other clause - `$where`, `$sort`, and each\n * populated relation's own query - stays the concrete {@link Query} it is today.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = QueryStreamProjection<E, S, V, X, P> & {\n // Intersecting the captured names with {@link QueryCount}'s own leaves a to-one relation no key\n // here at all, so counting one is an excess property rather than a value to check. Narrowing the\n // key rather than the value also instantiates `QueryCount<E>` once instead of once per counted\n // relation, worth ~87k instantiations in a consuming project.\n $count?: { [K in C & keyof QueryCount<E>]?: QueryCount<E>[K] };\n};\n\n/**\n * {@link QueryProjection} without `$count`, which a stream cannot honor. Split out rather than\n * subtracted afterwards: an optional key is a *known* key even when its value maps over `never`, so\n * a statement that must not take the clause has to be built without it in the first place.\n * @internal\n */\ntype QueryStreamProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = {\n $select?: { [K in S]?: V } | readonly QueryRaw[];\n $exclude?: { [K in X]?: V };\n $populate?: { [K in P]?: QueryPopulate<E>[K] };\n};\n\n/**\n * A {@link QueryProjected} a stream can honor: no `$count`, which is batched over a result set a\n * stream never holds all of.\n */\nexport type QueryStreamProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = Except<\n Query<E>,\n '$count'\n> &\n QueryStreamProjection<E, S, V, X, P>;\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n> = Query<E> & QueryProjection<E, S, V, X, P, C>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;\n\n/**\n * The keys a query comes back with, mirroring what the runtime projects: the fields a positive\n * `$select` names, or every field minus what a falsy `$select` entry or a truthy `$exclude` entry\n * subtracts, plus the relations `$populate` asked for. A positive `$select` wins outright, which is\n * why `$exclude` is only read on the branch where there is none.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P, C> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P\n // Populating a relation keeps the id whatever the projection says, and so does counting one: both\n // assemble their result by it (`selectFields` puts it back, as does MongoDB's `pipelineProjection`).\n | ([P | C] extends [never] ? never : NamedIdKey<E>);\n\n/**\n * The id key when it can be named, and nothing when it cannot: {@link IdKey} widens to *every* field\n * for an entity whose id is neither branded nor called `id`/`_id`/`uuid`, and adding that back would\n * hand the caller a row claiming fields the query never fetched. Missing an id costs a `$select`\n * entry; promising absent fields is the bug this type exists to prevent.\n * @internal\n */\ntype NamedIdKey<E> = [FieldKey<E>] extends [IdKey<E>] ? never : IdKey<E>;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A row of a find result: the entity narrowed to the fields the query projected, plus the relations\n * it populated - reading anything the query left out is a compile error rather than a silent\n * `undefined`. Modifiers are preserved, so an optional field stays optional. Name a projected row\n * with it where a helper has to take one: `QueryFindResult<User, 'id' | 'name'>`.\n *\n * The entity itself when the query projects nothing, when it uses a raw-projection array (columns,\n * not fields), and when the projection is not uniform - a `Query<E>` built elsewhere, or a map\n * mixing selected and subtracted entries, whose positive keys inference cannot recover. Relations\n * keep their declared type: narrowing them means capturing their queries as maps, which costs those\n * queries their own checks.\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? { [K in keyof E as K extends ProjectedKeys<E, S, V, X, P, C> ? K : never]: E[K] }\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n {\n [K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> ? K : never]: E[K];\n } & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/**\n * result of an update operation.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the inserted IDs, in insertion order. Exact on `'returning'` dialects; inferred from the\n * driver header on the others (see {@link InsertIdSource}), and empty when the header\n * reports no generated ID.\n */\n ids?: PrimaryKey[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
|
|
11
11
|
"import type { Query, QueryOptions } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.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\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>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof Query<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof Query<unknown>)[]);\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 (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\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 // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\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",
|
|
12
12
|
"import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityData,\n FieldKey,\n IdValue,\n Query,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdatePayload,\n} 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<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n id: IdValue<E>,\n q?: QueryOneProjected<E, S, V, X, P, C>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<QueryFindResult<E, S, V, X, P, C> | undefined>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n findOne<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryOneProjected<E, S, V, X, P, C>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n return this.read<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`,\n q,\n opts,\n );\n }\n\n findMany<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: Query<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<QueryFindResult<E, S, V, X, P, C>[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\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?: QueryPage<E>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n /** The `count` route capped at one row, so existence needs no endpoint of its own. */\n async exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: RequestOptions) {\n const res = await this.count(entity, { ...q, $limit: 1 }, opts);\n return { ...res, data: res.data > 0 };\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: EntityData<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: EntityData<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: EntityData<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: EntityData<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",
|
|
13
13
|
"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"
|
|
@@ -5,9 +5,9 @@ import { MySqlDialect } from '../mysql/mysqlDialect.js';
|
|
|
5
5
|
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
6
6
|
import { getAffectedRows, inferDialectName, isPoolableDialect, normalizeBunOpts, normalizeRows, } from './bunSql.util.js';
|
|
7
7
|
import { BunSqlCockroachDialect } from './bunSqlCockroachDialect.js';
|
|
8
|
-
import { BunSqliteDialect } from './bunSqliteDialect.js';
|
|
9
8
|
import { BunSqlPostgresDialect } from './bunSqlPostgresDialect.js';
|
|
10
9
|
import { BunSqlQuerier } from './bunSqlQuerier.js';
|
|
10
|
+
import { BunSqliteDialect } from './bunSqliteDialect.js';
|
|
11
11
|
const DialectMap = {
|
|
12
12
|
postgres: BunSqlPostgresDialect,
|
|
13
13
|
mysql: MySqlDialect,
|
|
@@ -137,7 +137,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
137
137
|
return;
|
|
138
138
|
if (field.virtual) {
|
|
139
139
|
this.getRawValue(ctx, {
|
|
140
|
-
value:
|
|
140
|
+
value: field.virtual.as(key),
|
|
141
141
|
prefix: opts.prefix,
|
|
142
142
|
escapedPrefix,
|
|
143
143
|
autoPrefixAlias: opts.autoPrefixAlias,
|
|
@@ -812,7 +812,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
812
812
|
// off it explicitly rather than forwarding `q` itself into `search()`, which would honor a
|
|
813
813
|
// `$sort`/`$skip`/`$limit` an untyped caller snuck in regardless of what TypeScript allowed them.
|
|
814
814
|
count(ctx, entity, q, opts) {
|
|
815
|
-
this.select(ctx, entity, { $select: [raw
|
|
815
|
+
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(COUNT_ALIAS)] });
|
|
816
816
|
this.search(ctx, entity, { $where: q.$where }, opts);
|
|
817
817
|
}
|
|
818
818
|
/**
|
|
@@ -1321,22 +1321,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1321
1321
|
}
|
|
1322
1322
|
getRawValue(ctx, opts) {
|
|
1323
1323
|
const { value, prefix = '', escapedPrefix, autoPrefixAlias } = opts;
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
escapedPrefix: escapedPrefix ?? this.escapeId(prefix, true, true),
|
|
1332
|
-
});
|
|
1333
|
-
if (typeof res === 'string' || (typeof res === 'number' && !Number.isNaN(res))) {
|
|
1334
|
-
ctx.append(String(res));
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
else {
|
|
1338
|
-
ctx.append(prefix + String(rawValue));
|
|
1339
|
-
}
|
|
1324
|
+
value.render({
|
|
1325
|
+
...opts,
|
|
1326
|
+
ctx,
|
|
1327
|
+
dialect: this,
|
|
1328
|
+
prefix,
|
|
1329
|
+
escapedPrefix: escapedPrefix ?? this.escapeId(prefix, true, true),
|
|
1330
|
+
});
|
|
1340
1331
|
const alias = value[RAW_ALIAS];
|
|
1341
1332
|
if (alias) {
|
|
1342
1333
|
const fullAlias = autoPrefixAlias && prefix ? `${prefix}.${alias}` : alias;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
-
import { getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst } from '../../util/index.js';
|
|
2
|
+
import { getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, normalizeIndexWhere, upperFirst, } from '../../util/index.js';
|
|
3
3
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
4
4
|
// Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
|
|
5
5
|
// evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
|
|
@@ -20,7 +20,7 @@ export function defineField(entity, key, opts = {}) {
|
|
|
20
20
|
// column from the referenced primary key (picking up its `columnType`, length and chained keys)
|
|
21
21
|
// instead of treating whatever ends up in `type` as deliberate.
|
|
22
22
|
const resolved = opts.type ? opts : { ...opts, typeFromReference: true };
|
|
23
|
-
meta.fields[fieldKey] = { ...meta.fields[fieldKey],
|
|
23
|
+
meta.fields[fieldKey] = { ...meta.fields[fieldKey], name: key, ...resolved };
|
|
24
24
|
return meta;
|
|
25
25
|
}
|
|
26
26
|
export function defineId(entity, key, opts) {
|
|
@@ -63,7 +63,12 @@ export function defineIndex(entity, index) {
|
|
|
63
63
|
const meta = ensureMeta(entity);
|
|
64
64
|
if (!meta.indexes)
|
|
65
65
|
meta.indexes = [];
|
|
66
|
-
meta.indexes.push({
|
|
66
|
+
meta.indexes.push({
|
|
67
|
+
...index,
|
|
68
|
+
unique: index.unique ?? false,
|
|
69
|
+
where: normalizeIndexWhere(index.where),
|
|
70
|
+
columns: index.columns.map(normalizeIndexColumn),
|
|
71
|
+
});
|
|
67
72
|
return meta;
|
|
68
73
|
}
|
|
69
74
|
export function defineFilter(entity, name, opts) {
|
package/dist/http/handler.js
CHANGED
|
@@ -26,7 +26,7 @@ export function createRequestHandler(opts) {
|
|
|
26
26
|
throw new TypeError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
|
|
27
27
|
"A route is the kebab-cased class name. Rename a class, or pass only one of them in 'include'.");
|
|
28
28
|
}
|
|
29
|
-
//
|
|
29
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- heterogeneous entity map
|
|
30
30
|
const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
|
|
31
31
|
return (req) => {
|
|
32
32
|
const entity = entityByPath.get(req.entityPath);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OperationRecorder: Record operations only (for code generation)
|
|
6
6
|
* - MigrationBuilder: Execute DDL operations (for integration tests/runtime)
|
|
7
7
|
*/
|
|
8
|
-
import { normalizeIndexColumn } from '../../util/index.js';
|
|
8
|
+
import { normalizeIndexColumn, normalizeIndexWhere } from '../../util/index.js';
|
|
9
9
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
10
10
|
import { createSchemaGenerator } from '../schemaGenerator.js';
|
|
11
11
|
import { splitSqlStatements } from './splitSqlStatements.js';
|
|
@@ -29,6 +29,7 @@ function createIndexOperation(tableName, columns, options = {}) {
|
|
|
29
29
|
...index,
|
|
30
30
|
name: name ??
|
|
31
31
|
derivedIndexName(tableName, entries.map((entry) => entry.column)),
|
|
32
|
+
where: normalizeIndexWhere(index.where),
|
|
32
33
|
entries,
|
|
33
34
|
unique: unique ?? false,
|
|
34
35
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fluent API for defining tables in migrations.
|
|
5
5
|
*/
|
|
6
|
-
import { normalizeIndexColumn } from '../../util/index.js';
|
|
6
|
+
import { normalizeIndexColumn, normalizeIndexWhere } from '../../util/index.js';
|
|
7
7
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
8
8
|
import { ColumnBuilder } from './columnBuilder.js';
|
|
9
9
|
import { expr } from './expressions.js';
|
|
@@ -171,6 +171,7 @@ export class TableBuilder {
|
|
|
171
171
|
this._indexes.push({
|
|
172
172
|
...rest,
|
|
173
173
|
name: name ?? `${prefix}_${this._name}_${entries.map((entry) => entry.column).join('_')}`,
|
|
174
|
+
where: normalizeIndexWhere(rest.where),
|
|
174
175
|
entries,
|
|
175
176
|
unique,
|
|
176
177
|
});
|
|
@@ -74,7 +74,7 @@ export function buildIndexDecoratorSource(index, propertyName) {
|
|
|
74
74
|
if (distance)
|
|
75
75
|
options.push(`distance: '${distance}'`);
|
|
76
76
|
if (index.where)
|
|
77
|
-
options.push(`where: ${
|
|
77
|
+
options.push(`where: ${rawTag(index.where)}`);
|
|
78
78
|
if (index.include?.length) {
|
|
79
79
|
options.push(`include: [${index.include.map((column) => `'${propertyName(column)}'`).join(', ')}]`);
|
|
80
80
|
}
|
|
@@ -82,11 +82,11 @@ export function buildIndexDecoratorSource(index, propertyName) {
|
|
|
82
82
|
}
|
|
83
83
|
/** Whether emitting this index needs `raw` imported alongside `Index`. */
|
|
84
84
|
export function indexNeedsRaw(index) {
|
|
85
|
-
return index.entries.some((entry) => entry.expression);
|
|
85
|
+
return Boolean(index.where) || index.entries.some((entry) => entry.expression);
|
|
86
86
|
}
|
|
87
87
|
function indexEntrySource(entry, propertyName) {
|
|
88
88
|
if (entry.expression) {
|
|
89
|
-
return
|
|
89
|
+
return rawTag(entry.column);
|
|
90
90
|
}
|
|
91
91
|
const modifiers = significantModifiers(entry);
|
|
92
92
|
if (modifiers.length === 0) {
|
|
@@ -95,11 +95,11 @@ function indexEntrySource(entry, propertyName) {
|
|
|
95
95
|
return `{ column: '${propertyName(entry.column)}', ${modifiers.join(', ')} }`;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* SQL as a
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* compile or, worse, compiles to a different index.
|
|
98
|
+
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
99
|
+
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
|
100
|
+
* Newlines need none, which keeps a multi-line expression readable in the generated entity.
|
|
102
101
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
102
|
+
function rawTag(sql) {
|
|
103
|
+
const escaped = sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
104
|
+
return `raw\`${escaped}\``;
|
|
105
105
|
}
|
|
@@ -147,7 +147,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
147
147
|
async internalAggregate(entity, q, opts) {
|
|
148
148
|
return this.timed('internalAggregate', undefined, async () => {
|
|
149
149
|
const pipeline = this.dialect.buildAggregateStages(entity, q, opts);
|
|
150
|
-
//
|
|
150
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- aggregate result type matches QueryAggregateResult at runtime but TS can't verify
|
|
151
151
|
return this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
152
152
|
});
|
|
153
153
|
}
|
|
@@ -265,7 +265,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
265
265
|
async internalAggregate(entity, q, opts) {
|
|
266
266
|
const ctx = this.dialect.createContext();
|
|
267
267
|
this.dialect.aggregate(ctx, entity, q, opts);
|
|
268
|
-
//
|
|
268
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- raw DB rows satisfy QueryAggregateResult at runtime but TS can't verify
|
|
269
269
|
const res = await this.all(ctx.sql, ctx.values);
|
|
270
270
|
const hydratable = this.dialect.hydratableAggregates(entity, q);
|
|
271
271
|
for (const row of res) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency ordering over any schema object, not just tables: a new kind joins the ordering by
|
|
3
|
+
* describing its edges rather than by adding a branch here.
|
|
4
|
+
*/
|
|
5
|
+
export type DependenciesOf<N> = (node: N) => Iterable<N>;
|
|
6
|
+
/**
|
|
7
|
+
* Nodes in creation order, dependencies first. Cycle-tolerant by design: a cyclic foreign key is
|
|
8
|
+
* legal SQL, handled by deferring the constraint, so a cycle orders arbitrarily rather than
|
|
9
|
+
* throwing. Use {@link findCycles} to report one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createOrder<N>(nodes: Iterable<N>, dependenciesOf: DependenciesOf<N>): N[];
|
|
12
|
+
/** Nodes in drop order, dependents first. */
|
|
13
|
+
export declare function dropOrder<N>(nodes: Iterable<N>, dependenciesOf: DependenciesOf<N>): N[];
|
|
14
|
+
/**
|
|
15
|
+
* Every dependency cycle, each starting where it closes. A separate walk from {@link createOrder}
|
|
16
|
+
* rather than a flag on it: ordering must succeed on any graph, reporting must see every cycle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findCycles<N>(nodes: Iterable<N>, dependenciesOf: DependenciesOf<N>): N[][];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency ordering over any schema object, not just tables: a new kind joins the ordering by
|
|
3
|
+
* describing its edges rather than by adding a branch here.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Nodes in creation order, dependencies first. Cycle-tolerant by design: a cyclic foreign key is
|
|
7
|
+
* legal SQL, handled by deferring the constraint, so a cycle orders arbitrarily rather than
|
|
8
|
+
* throwing. Use {@link findCycles} to report one.
|
|
9
|
+
*/
|
|
10
|
+
export function createOrder(nodes, dependenciesOf) {
|
|
11
|
+
const ordered = [];
|
|
12
|
+
const visited = new Set();
|
|
13
|
+
const visit = (node) => {
|
|
14
|
+
if (visited.has(node)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
visited.add(node);
|
|
18
|
+
for (const dependency of dependenciesOf(node)) {
|
|
19
|
+
visit(dependency);
|
|
20
|
+
}
|
|
21
|
+
ordered.push(node);
|
|
22
|
+
};
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
visit(node);
|
|
25
|
+
}
|
|
26
|
+
return ordered;
|
|
27
|
+
}
|
|
28
|
+
/** Nodes in drop order, dependents first. */
|
|
29
|
+
export function dropOrder(nodes, dependenciesOf) {
|
|
30
|
+
return createOrder(nodes, dependenciesOf).reverse();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every dependency cycle, each starting where it closes. A separate walk from {@link createOrder}
|
|
34
|
+
* rather than a flag on it: ordering must succeed on any graph, reporting must see every cycle.
|
|
35
|
+
*/
|
|
36
|
+
export function findCycles(nodes, dependenciesOf) {
|
|
37
|
+
const cycles = [];
|
|
38
|
+
const visited = new Set();
|
|
39
|
+
const onPath = new Set();
|
|
40
|
+
const visit = (node, path) => {
|
|
41
|
+
if (onPath.has(node)) {
|
|
42
|
+
const start = path.indexOf(node);
|
|
43
|
+
if (start !== -1) {
|
|
44
|
+
cycles.push(path.slice(start));
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (visited.has(node)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
visited.add(node);
|
|
52
|
+
onPath.add(node);
|
|
53
|
+
for (const dependency of dependenciesOf(node)) {
|
|
54
|
+
visit(dependency, [...path, node]);
|
|
55
|
+
}
|
|
56
|
+
onPath.delete(node);
|
|
57
|
+
};
|
|
58
|
+
for (const node of nodes) {
|
|
59
|
+
visit(node, []);
|
|
60
|
+
}
|
|
61
|
+
return cycles;
|
|
62
|
+
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { areTypesEqual, canonicalToColumnType, canonicalToSql, canonicalToTypeSc
|
|
|
17
17
|
* @returns The SchemaAST representing the database schema
|
|
18
18
|
*/
|
|
19
19
|
export declare function introspectSchema(introspector: SchemaIntrospector): Promise<SchemaAST>;
|
|
20
|
+
export { createOrder, dropOrder, findCycles, type DependenciesOf } from './dependencyGraph.js';
|
|
20
21
|
export { SchemaAST } from './schemaAST.js';
|
|
21
22
|
export type { BuildSchemaASTOptions } from './schemaASTBuilder.js';
|
|
22
23
|
export { buildSchemaAST } from './schemaASTBuilder.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export async function introspectSchema(introspector) {
|
|
|
19
19
|
return introspector.introspect();
|
|
20
20
|
}
|
|
21
21
|
// SchemaAST class
|
|
22
|
+
export { createOrder, dropOrder, findCycles } from './dependencyGraph.js';
|
|
22
23
|
export { SchemaAST } from './schemaAST.js';
|
|
23
24
|
// Builder
|
|
24
25
|
export { buildSchemaAST } from './schemaASTBuilder.js';
|
|
@@ -87,11 +87,6 @@ export declare class SchemaAST implements ISchemaAST {
|
|
|
87
87
|
* Tables that depend on others come first, then the tables they depend on.
|
|
88
88
|
*/
|
|
89
89
|
getDropOrder(): TableNode[];
|
|
90
|
-
/**
|
|
91
|
-
* Topological sort respecting FK dependencies.
|
|
92
|
-
* Uses Kahn's algorithm for stable ordering.
|
|
93
|
-
*/
|
|
94
|
-
private topologicalSort;
|
|
95
90
|
/**
|
|
96
91
|
* Validate schema integrity.
|
|
97
92
|
* Checks for:
|
package/dist/schema/schemaAST.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Provides graph operations like navigation, validation, and topological sorting.
|
|
6
6
|
*/
|
|
7
7
|
import { qualifyName } from '../util/sql.util.js';
|
|
8
|
+
import { createOrder, dropOrder, findCycles } from './dependencyGraph.js';
|
|
8
9
|
/**
|
|
9
10
|
* A table node with its collections empty, ready to be filled. Six places build one, and the fields
|
|
10
11
|
* that are pure boilerplate are exactly the ones a new field gets forgotten in: {@link TableNode.schema}
|
|
@@ -120,30 +121,7 @@ export class SchemaAST {
|
|
|
120
121
|
* Returns arrays of tables that form cycles.
|
|
121
122
|
*/
|
|
122
123
|
detectCircularDependencies() {
|
|
123
|
-
|
|
124
|
-
const visited = new Set();
|
|
125
|
-
const stack = new Set();
|
|
126
|
-
const dfs = (table, path) => {
|
|
127
|
-
if (stack.has(table)) {
|
|
128
|
-
const cycleStart = path.indexOf(table);
|
|
129
|
-
if (cycleStart !== -1) {
|
|
130
|
-
cycles.push(path.slice(cycleStart));
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (visited.has(table))
|
|
135
|
-
return;
|
|
136
|
-
visited.add(table);
|
|
137
|
-
stack.add(table);
|
|
138
|
-
for (const dep of this.getDependencies(table)) {
|
|
139
|
-
dfs(dep, [...path, table]);
|
|
140
|
-
}
|
|
141
|
-
stack.delete(table);
|
|
142
|
-
};
|
|
143
|
-
for (const table of this.tables.values()) {
|
|
144
|
-
dfs(table, []);
|
|
145
|
-
}
|
|
146
|
-
return cycles;
|
|
124
|
+
return findCycles(this.tables.values(), (table) => this.getDependencies(table));
|
|
147
125
|
}
|
|
148
126
|
/**
|
|
149
127
|
* Check if there are any circular dependencies.
|
|
@@ -156,37 +134,14 @@ export class SchemaAST {
|
|
|
156
134
|
* Tables with no dependencies come first, then tables that depend on them, etc.
|
|
157
135
|
*/
|
|
158
136
|
getCreateOrder() {
|
|
159
|
-
return this.
|
|
137
|
+
return createOrder(this.tables.values(), (table) => this.getDependencies(table));
|
|
160
138
|
}
|
|
161
139
|
/**
|
|
162
140
|
* Get tables in correct order for DROP (dependents first).
|
|
163
141
|
* Tables that depend on others come first, then the tables they depend on.
|
|
164
142
|
*/
|
|
165
143
|
getDropOrder() {
|
|
166
|
-
return this.
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Topological sort respecting FK dependencies.
|
|
170
|
-
* Uses Kahn's algorithm for stable ordering.
|
|
171
|
-
*/
|
|
172
|
-
topologicalSort() {
|
|
173
|
-
const result = [];
|
|
174
|
-
const visited = new Set();
|
|
175
|
-
const visit = (table) => {
|
|
176
|
-
if (visited.has(table))
|
|
177
|
-
return;
|
|
178
|
-
visited.add(table);
|
|
179
|
-
// Visit dependencies first (tables this table references)
|
|
180
|
-
for (const dep of this.getDependencies(table)) {
|
|
181
|
-
visit(dep);
|
|
182
|
-
}
|
|
183
|
-
result.push(table);
|
|
184
|
-
};
|
|
185
|
-
// Visit all tables
|
|
186
|
-
for (const table of this.tables.values()) {
|
|
187
|
-
visit(table);
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
144
|
+
return dropOrder(this.tables.values(), (table) => this.getDependencies(table));
|
|
190
145
|
}
|
|
191
146
|
/**
|
|
192
147
|
* Validate schema integrity.
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -147,7 +147,7 @@ export type JsonUpdateOp<T = unknown> = {
|
|
|
147
147
|
type JsonUpdateOpFor<V, T = UnwrapJson<NonNullable<V>>> = [T] extends [never] ? never : IsMany<T> extends true ? never : JsonUpdateOp<T>;
|
|
148
148
|
/**
|
|
149
149
|
* Accepted value for a single field in an update payload: the value itself, `null` where the column
|
|
150
|
-
* is nullable, `QueryRaw` for a raw SQL expression (e.g. `
|
|
150
|
+
* is nullable, `QueryRaw` for a raw SQL expression (e.g. ``raw`NOW()` ``), and - for JSON object
|
|
151
151
|
* fields - the JSON operators.
|
|
152
152
|
*
|
|
153
153
|
* An optional property is a nullable column, and clearing one is what an update is for, so `null`
|
|
@@ -522,7 +522,7 @@ export type IndexTypeOptions = {
|
|
|
522
522
|
* @example
|
|
523
523
|
* ```ts
|
|
524
524
|
* @Index(['tenantId', { column: 'createdAt', order: 'desc' }]) // keyset pagination
|
|
525
|
-
* @Index([raw
|
|
525
|
+
* @Index([raw`lower("email")`], { unique: true }) // case-insensitive uniqueness
|
|
526
526
|
* @Index([{ column: 'body', length: 64 }]) // MySQL needs a prefix on TEXT
|
|
527
527
|
* @Index(['data'], { type: 'gin' }) // JSONB containment
|
|
528
528
|
* ```
|
|
@@ -734,9 +734,14 @@ export type EntityOptions<E = unknown> = {
|
|
|
734
734
|
* migration builder's `table.index(...)`. `Except` (not plain `Omit`) keeps `type`/`distance` a
|
|
735
735
|
* discriminated pair: omitting `distance` on a vector index type is a compile error.
|
|
736
736
|
*/
|
|
737
|
-
export type IndexOptions<E = unknown> = Except<EntityIndexMeta, 'columns' | 'include'> & {
|
|
737
|
+
export type IndexOptions<E = unknown> = Except<EntityIndexMeta, 'columns' | 'include' | 'where'> & {
|
|
738
738
|
/** Non-key columns stored in the index; a typo builds nothing, the server refusing the statement. */
|
|
739
739
|
readonly include?: readonly IndexFieldKey<E>[];
|
|
740
|
+
/**
|
|
741
|
+
* Partial-index predicate. `raw` with no interpolation, like an index expression: this is DDL, so
|
|
742
|
+
* there is no placeholder for a bound value. A bare string is the older spelling and still works.
|
|
743
|
+
*/
|
|
744
|
+
readonly where?: string | QueryRaw;
|
|
740
745
|
};
|
|
741
746
|
/** A field of `E`, or any name where there is no entity to check it against - the migration builder. */
|
|
742
747
|
type IndexFieldKey<E> = unknown extends E ? string : FieldKey<E>;
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -134,7 +134,7 @@ export interface IndexSchema extends VectorIndexOptions {
|
|
|
134
134
|
readonly name: string;
|
|
135
135
|
/**
|
|
136
136
|
* What the index is over, in order. Named `entries` and not `columns` because an entry need not be
|
|
137
|
-
* a column at all: `
|
|
137
|
+
* a column at all: ``raw`lower(email)` `` is one, and so is a column carrying a prefix length or a
|
|
138
138
|
* stored order. The authored form, `@Index([...])`, still spells this `columns`, since that is what
|
|
139
139
|
* it reads like at the call site.
|
|
140
140
|
*/
|
package/dist/type/query.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type QuerySelect<E> = {
|
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
47
|
* Accepted `$select` value: a field map, or raw SQL projections built with `raw()`
|
|
48
|
-
* (e.g.
|
|
48
|
+
* (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.
|
|
49
49
|
*/
|
|
50
50
|
export type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];
|
|
51
51
|
/**
|
|
@@ -208,7 +208,7 @@ export type QuerySearch<E> = QueryPage<E> & {
|
|
|
208
208
|
export type Query<E> = {
|
|
209
209
|
/**
|
|
210
210
|
* field selection - `{ name: true }` whitelists fields, or raw SQL projections
|
|
211
|
-
* (
|
|
211
|
+
* (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).
|
|
212
212
|
* Mutually exclusive with `$exclude`.
|
|
213
213
|
*/
|
|
214
214
|
$select?: QuerySelectValue<E>;
|
package/dist/type/queryRaw.d.ts
CHANGED
|
@@ -38,4 +38,16 @@ export declare class QueryRaw {
|
|
|
38
38
|
readonly [RAW_VALUE]: Scalar | QueryRawFn;
|
|
39
39
|
readonly [RAW_ALIAS]?: string;
|
|
40
40
|
constructor(value: Scalar | QueryRawFn, alias?: string);
|
|
41
|
+
/** The same expression under an alias, for a `$select` projection. */
|
|
42
|
+
as(alias: string): QueryRaw;
|
|
43
|
+
/**
|
|
44
|
+
* Emit this expression into `opts.ctx`. How a raw value becomes SQL is the raw value's own
|
|
45
|
+
* business, which is what lets a `raw` tagged template resolve an interpolated fragment without
|
|
46
|
+
* the dialect having to expose a method for it.
|
|
47
|
+
*
|
|
48
|
+
* The alias is not emitted here: it belongs to a `$select` projection, not to an expression, and
|
|
49
|
+
* a fragment nested inside another would otherwise emit one mid-expression. `getRawValue` appends
|
|
50
|
+
* it around this call.
|
|
51
|
+
*/
|
|
52
|
+
render(opts: Required<QueryRawFnOptions>): void;
|
|
41
53
|
}
|
package/dist/type/queryRaw.js
CHANGED
|
@@ -7,4 +7,28 @@ export class QueryRaw {
|
|
|
7
7
|
this[RAW_VALUE] = value;
|
|
8
8
|
this[RAW_ALIAS] = alias;
|
|
9
9
|
}
|
|
10
|
+
/** The same expression under an alias, for a `$select` projection. */
|
|
11
|
+
as(alias) {
|
|
12
|
+
return new QueryRaw(this[RAW_VALUE], alias);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Emit this expression into `opts.ctx`. How a raw value becomes SQL is the raw value's own
|
|
16
|
+
* business, which is what lets a `raw` tagged template resolve an interpolated fragment without
|
|
17
|
+
* the dialect having to expose a method for it.
|
|
18
|
+
*
|
|
19
|
+
* The alias is not emitted here: it belongs to a `$select` projection, not to an expression, and
|
|
20
|
+
* a fragment nested inside another would otherwise emit one mid-expression. `getRawValue` appends
|
|
21
|
+
* it around this call.
|
|
22
|
+
*/
|
|
23
|
+
render(opts) {
|
|
24
|
+
const value = this[RAW_VALUE];
|
|
25
|
+
if (typeof value !== 'function') {
|
|
26
|
+
opts.ctx.append(opts.prefix + String(value));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const emitted = value(opts);
|
|
30
|
+
if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {
|
|
31
|
+
opts.ctx.append(String(emitted));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
10
34
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type IndexColumnInput, type IndexColumnSchema } from '../type/index.js';
|
|
1
|
+
import { type IndexColumnInput, type IndexColumnSchema, QueryRaw } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Reduces an authored index entry to its normalized form, so the three shapes users write - a column
|
|
4
4
|
* name, `raw(expression)`, or an options object - reach the dialects as one.
|
|
5
5
|
*/
|
|
6
6
|
export declare function normalizeIndexColumn(entry: IndexColumnInput): IndexColumnSchema;
|
|
7
|
+
/** The partial-index predicate, as authored: `raw` for new code, a bare string for old. */
|
|
8
|
+
export declare function normalizeIndexWhere(where: string | QueryRaw | undefined): string | undefined;
|
|
@@ -13,14 +13,18 @@ export function normalizeIndexColumn(entry) {
|
|
|
13
13
|
const { column, ...rest } = entry;
|
|
14
14
|
return column instanceof QueryRaw ? { ...rest, column: rawSql(column), expression: true } : { ...rest, column };
|
|
15
15
|
}
|
|
16
|
+
/** The partial-index predicate, as authored: `raw` for new code, a bare string for old. */
|
|
17
|
+
export function normalizeIndexWhere(where) {
|
|
18
|
+
return where instanceof QueryRaw ? rawSql(where, 'a partial-index predicate') : where;
|
|
19
|
+
}
|
|
16
20
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* Index DDL is evaluated once at creation time, so it cannot take the dialect-aware callback form of
|
|
22
|
+
* `raw()` - there is no query context to hand it, and no placeholder a `CREATE INDEX` could bind.
|
|
19
23
|
*/
|
|
20
|
-
function rawSql(value) {
|
|
24
|
+
function rawSql(value, what = 'an index expression') {
|
|
21
25
|
const sql = value[RAW_VALUE];
|
|
22
26
|
if (typeof sql !== 'string') {
|
|
23
|
-
throw new TypeError(
|
|
27
|
+
throw new TypeError(`${what} needs raw() with no interpolation, not a function or a bound value`);
|
|
24
28
|
}
|
|
25
29
|
return sql;
|
|
26
30
|
}
|
package/dist/util/raw.d.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import { QueryRaw, type QueryRawFn, type Scalar } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Create a raw SQL expression
|
|
3
|
+
* Create a raw SQL expression.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* introduce SQL injection vulnerabilities. Use parameterized queries
|
|
8
|
-
* (e.g. `$where` operators) for any user-supplied data.
|
|
5
|
+
* As a tagged template the literal text is emitted as written and every interpolation is resolved by
|
|
6
|
+
* what it is, so a value cannot become SQL whatever it holds:
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* | Interpolated | Becomes |
|
|
9
|
+
* | :----------------- | :---------------------- |
|
|
10
|
+
* | any value | a bound parameter |
|
|
11
|
+
* | a {@link QueryRaw} | that fragment, in place |
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* raw`GREATEST(0, "creditsAllowance" - ${amount})`
|
|
15
|
+
* raw`CONCAT(${col('firstName')}, ' ', ${col('lastName')})`
|
|
16
|
+
* raw`LOG10(${points})`.as('score')
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The callback form remains for SQL a template cannot express, such as a sub-query generated through
|
|
20
|
+
* `dialect.find(...)`. See {@link col} for a context-aware column reference.
|
|
21
|
+
*
|
|
22
|
+
* **⚠️ Security:** the tag is safe because it binds; the other two forms are not. `raw('SQL')` emits
|
|
23
|
+
* its argument verbatim and a callback emits whatever it writes, so build neither from user input.
|
|
24
|
+
* Inside a callback, bind with `ctx.addValue()`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function raw(strings: TemplateStringsArray, ...values: readonly unknown[]): QueryRaw;
|
|
27
|
+
export declare function raw(value: QueryRawFn, alias?: string): QueryRaw;
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated Emits its argument verbatim, so it cannot bind a value. Use the tagged template:
|
|
30
|
+
* `raw('"a" > 1')` becomes `` raw`"a" > 1` ``, and `raw('LOG10(x)', 'score')` becomes
|
|
31
|
+
* `` raw`LOG10(x)`.as('score') ``. `npx uql-codemod` rewrites both.
|
|
32
|
+
*/
|
|
33
|
+
export declare function raw(value: Scalar, alias?: string): QueryRaw;
|
|
34
|
+
/**
|
|
35
|
+
* A column of the entity being queried, alias-qualified and escaped for the dialect. This is what a
|
|
36
|
+
* template cannot know on its own: the alias is decided while the statement is built, not where the
|
|
37
|
+
* expression is written.
|
|
38
|
+
*
|
|
39
|
+
* Takes the column name as it exists in the database, not the entity's field name: no entity metadata
|
|
40
|
+
* is in scope here, so a naming strategy is not applied for you. `escapedPrefix` already carries its
|
|
41
|
+
* trailing dot, which is the detail this exists to stop you getting wrong.
|
|
16
42
|
*/
|
|
17
|
-
export declare function
|
|
43
|
+
export declare function col(column: string): QueryRaw;
|
package/dist/util/raw.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { QueryRaw } from '../type/index.js';
|
|
2
|
+
export function raw(value, ...rest) {
|
|
3
|
+
const [alias] = rest;
|
|
4
|
+
if (!isTemplateStrings(value)) {
|
|
5
|
+
return new QueryRaw(value, typeof alias === 'string' ? alias : undefined);
|
|
6
|
+
}
|
|
7
|
+
if (!rest.length) {
|
|
8
|
+
// Nothing to bind, so this is the string form: keep it one, for the DDL paths that need to read
|
|
9
|
+
// the expression back as text (an index expression cannot carry a parameter).
|
|
10
|
+
return new QueryRaw(value[0] ?? '');
|
|
11
|
+
}
|
|
12
|
+
return new QueryRaw((opts) => {
|
|
13
|
+
const { ctx } = opts;
|
|
14
|
+
ctx.append(value[0] ?? '');
|
|
15
|
+
rest.forEach((interpolated, i) => {
|
|
16
|
+
if (interpolated instanceof QueryRaw) {
|
|
17
|
+
interpolated.render(opts);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
ctx.addValue(interpolated);
|
|
21
|
+
}
|
|
22
|
+
ctx.append(value[i + 1] ?? '');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
2
26
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* unsanitized user input directly as the `value` argument - doing so may
|
|
7
|
-
* introduce SQL injection vulnerabilities. Use parameterized queries
|
|
8
|
-
* (e.g. `$where` operators) for any user-supplied data.
|
|
27
|
+
* A column of the entity being queried, alias-qualified and escaped for the dialect. This is what a
|
|
28
|
+
* template cannot know on its own: the alias is decided while the statement is built, not where the
|
|
29
|
+
* expression is written.
|
|
9
30
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param value the raw value or a function that receives dialect context
|
|
14
|
-
* @param alias optional alias for the expression (used in SELECT)
|
|
15
|
-
* @returns a QueryRaw instance
|
|
31
|
+
* Takes the column name as it exists in the database, not the entity's field name: no entity metadata
|
|
32
|
+
* is in scope here, so a naming strategy is not applied for you. `escapedPrefix` already carries its
|
|
33
|
+
* trailing dot, which is the detail this exists to stop you getting wrong.
|
|
16
34
|
*/
|
|
17
|
-
export function
|
|
18
|
-
return new QueryRaw(
|
|
35
|
+
export function col(column) {
|
|
36
|
+
return new QueryRaw(({ escapedPrefix, dialect }) => escapedPrefix + dialect.escapeId(column, true));
|
|
37
|
+
}
|
|
38
|
+
/** A tag call passes the frozen strings array, which carries its own `raw` counterpart. */
|
|
39
|
+
function isTemplateStrings(value) {
|
|
40
|
+
return Array.isArray(value) && Array.isArray(Reflect.get(value, 'raw'));
|
|
19
41
|
}
|
package/dist/util/sqlLiteral.js
CHANGED
|
@@ -18,7 +18,7 @@ export function escapeSingleQuotes(val) {
|
|
|
18
18
|
}
|
|
19
19
|
const ansiStringLiteral = (val) => `'${escapeSingleQuotes(val)}'`;
|
|
20
20
|
// MySQL/MariaDB backslash-escape rather than doubling the quote. Mirrors the `sqlstring` map it replaced.
|
|
21
|
-
//
|
|
21
|
+
// oxlint-disable-next-line no-control-regex -- MySQL string literals must escape NUL, backspace and SUB
|
|
22
22
|
const MYSQL_SPECIALS = /[\0\b\t\n\r\x1a"'\\]/g;
|
|
23
23
|
const MYSQL_ESCAPES = {
|
|
24
24
|
'\0': '\\0',
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.40.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|