uql-orm 0.31.2 → 0.31.4
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 +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/mongo/mongoDialect.d.ts +10 -0
- package/dist/mongo/mongoDialect.js +32 -6
- package/dist/mongo/mongodbQuerier.d.ts +4 -3
- package/dist/mongo/mongodbQuerier.js +18 -16
- package/dist/nestjs/uqlModule.d.ts +5 -0
- package/dist/nestjs/uqlModule.js +5 -5
- package/dist/querier/abstractQuerier.d.ts +11 -4
- package/dist/querier/abstractQuerier.js +41 -30
- package/dist/querier/abstractSqlQuerier.d.ts +4 -2
- package/dist/querier/abstractSqlQuerier.js +23 -21
- package/dist/type/query.d.ts +25 -13
- package/dist/util/dialect.util.d.ts +14 -1
- package/dist/util/dialect.util.js +17 -0
- package/dist/util/object.util.d.ts +1 -3
- package/dist/util/object.util.js +0 -9
- package/dist/util/relationQuery.util.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ That is the whole install ([setup](https://uql-orm.dev/getting-started)), and th
|
|
|
37
37
|
<a href="https://uql-orm.dev">
|
|
38
38
|
<picture>
|
|
39
39
|
<source media="(prefers-color-scheme: dark)" srcset="https://uql-orm.dev/demo-dark.webp">
|
|
40
|
-
<img src="https://uql-orm.dev/demo-light.webp" alt="A UQL query being typed: the compiler underlines the misspelled 'emial', then 'titel' three levels deep inside $populate">
|
|
40
|
+
<img src="https://uql-orm.dev/demo-light.webp" alt="A UQL query being typed: the compiler underlines the misspelled 'emial', then 'titel' three levels deep inside $populate, then '$like' on a numeric column">
|
|
41
41
|
</picture>
|
|
42
42
|
</a>
|
|
43
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var W=[];function F(B){for(let V of W)V(B)}function _(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function N(B,V){return G(B,{method:"get"},V)}function Q(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},k=z($),
|
|
1
|
+
var W=[];function F(B){for(let V of W)V(B)}function _(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function N(B,V){return G(B,{method:"get"},V)}function Q(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},k=z($),R=new Map(k.filter((B)=>$[B].method==="GET"&&$[B].path!=="/:id").map((B)=>[$[B].path,B]));function D(B){return K(B.name)}var c=["$select","$populate","$exclude","$where","$sort"],v=new Set([...c,"$skip","$limit","hardDelete","count"]);function A(B){if(!B)return"";let V=new URLSearchParams;for(let X of z(B)){let Z=B[X];if(Z===void 0)continue;V.append(X,typeof Z==="object"&&Z!==null?JSON.stringify(Z):String(Z))}let I=V.toString();return I?`?${I}`:""}class C{basePath;defaults;constructor(B,V={}){this.basePath=B;this.defaults=V}findOneById(B,V,I,X){let Z=this.getBasePath(B),H=A(I);return N(`${Z}/${V}${H}`,this.buildOptions(X))}findOne(B,V,I){return this.read(`${this.getBasePath(B)}${$.findOne.path}`,V,I)}findMany(B,V,I){let X={...V};if(I?.count)X.count=!0;return this.read(this.getBasePath(B),X,I)}async findManyAndCount(B,V,I){let X=await this.findMany(B,V,{...I,count:!0});if(typeof X.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...X,count:X.count}}count(B,V,I){return this.read(`${this.getBasePath(B)}${$.count.path}`,V,I)}insertOne(B,V,I){let X=this.getBasePath(B);return Q(X,V,this.buildOptions(I))}insertMany(B,V,I){let X=this.getBasePath(B);return Q(`${X}${$.insertMany.path}`,V,this.buildOptions(I))}updateOneById(B,V,I,X){let Z=this.getBasePath(B);return J(`${Z}/${V}`,I,this.buildOptions(X))}updateMany(B,V,I,X){let Z=this.getBasePath(B),H=A(V);return J(`${Z}${H}`,I,this.buildOptions(X))}saveOne(B,V,I){let X=this.getBasePath(B);return L(X,V,this.buildOptions(I))}saveMany(B,V,I){let X=this.getBasePath(B);return L(`${X}${$.saveMany.path}`,V,this.buildOptions(I))}deleteOneById(B,V,I={}){let X=this.getBasePath(B),Z=I.hardDelete?A({hardDelete:I.hardDelete}):"";return x(`${X}/${V}${Z}`,this.buildOptions(I))}deleteMany(B,V,I={}){let X=this.getBasePath(B),Z=A(I.hardDelete?{...V,hardDelete:I.hardDelete}:V);return x(`${X}${Z}`,this.buildOptions(I))}getBasePath(B){return`${this.basePath}/${D(B)}`}read(B,V,I){if(this.defaults.readMethod==="QUERY")return T(B,V??{},this.buildOptions(I));return N(`${B}${A(V)}`,this.buildOptions(I))}buildOptions(B){if(!this.defaults.headers&&!B?.headers)return B;return{...B,headers:{...this.defaults.headers,...B?.headers}}}}var P={getQuerier:()=>new C("/api")};function y(B){P=B}function g(){return P}function p(){return g().getQuerier()}export{y as setQuerierPool,x as remove,T as query,L as put,Q as post,J as patch,_ as on,F as notify,g as getQuerierPool,p as getQuerier,N as get,f as RequestError,C as HttpQuerier};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=BB4E96BEA0CE335F64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=uql-browser.min.js.map
|
|
@@ -4,14 +4,14 @@
|
|
|
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, RequestSuccessResponse } from '../../http/contract.js';\nimport type { RequestOptions } from '../type/index.js';\nimport { notify } from './bus.js';\n\n/**\n * Error thrown for non-2xx responses. Carries the HTTP status so callers can key\n * behavior on it (401 redirects, 402 payment flows, error-boundary routing).\n */\nexport class RequestError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = 'RequestError';\n }\n}\n\nexport function get<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'get' }, opts);\n}\n\nexport function post<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'post', body }, opts);\n}\n\nexport function patch<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'patch', body }, opts);\n}\n\nexport function put<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'put', body }, opts);\n}\n\nexport function remove<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'delete' }, opts);\n}\n\n/**\n * HTTP QUERY (RFC 10008): a safe, idempotent read whose JSON query travels in the\n * request body, avoiding URL-length limits. Method name must stay uppercase\n * (fetch only normalizes the classic verbs).\n */\nexport function query<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'QUERY', body }, opts);\n}\n\nfunction request<T>(url: string, init: RequestInit, opts?: RequestOptions) {\n notify({ phase: 'start', opts });\n\n init.headers = {\n accept: 'application/json',\n 'content-type': 'application/json',\n ...opts?.headers,\n };\n if (opts?.signal) {\n init.signal = opts.signal;\n }\n\n return fetch(url, init)\n .then((rawResp) =>\n rawResp.json().then((resp: unknown) => {\n const isSuccess = rawResp.status >= 200 && rawResp.status < 300;\n if (isSuccess) {\n notify({ phase: 'success', opts });\n return resp as RequestSuccessResponse<T>;\n }\n const errorResp = resp as Partial<RequestErrorResponse> | undefined;\n const error = {\n message: errorResp?.error?.message ?? rawResp.statusText,\n code: errorResp?.error?.code ?? rawResp.status,\n };\n notify({ phase: 'error', error, opts });\n throw new RequestError(error.message, error.code);\n }),\n )\n .finally(() => {\n notify({ phase: 'complete', opts });\n });\n}\n",
|
|
7
|
-
"import type { FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n
|
|
7
|
+
"import type { FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\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\nexport function getFieldKeys<E>(\n fields: {\n [K in FieldKey<E>]?: FieldOptions;\n },\n): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\n}\n",
|
|
8
8
|
"export function kebabCase(val: string): string {\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n resp += val[i] === val[i].toUpperCase() ? '-' + val[i].toLowerCase() : val[i];\n }\n return resp;\n}\n\nexport function upperFirst(text: string): string {\n if (!text) return text;\n return text[0].toUpperCase() + text.slice(1);\n}\n\nexport function lowerFirst(text: string): string {\n if (!text) return text;\n return text[0].toLowerCase() + text.slice(1);\n}\n\nexport function snakeCase(val: string): string {\n if (val === null || val === undefined) return val as string;\n if (!val) return '';\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n const char = val[i];\n const charLower = char.toLowerCase();\n if (char !== charLower && char === char.toUpperCase()) {\n resp += '_' + charLower;\n } else {\n resp += char;\n }\n }\n return resp;\n}\n\n/**\n * Convert a string to PascalCase (UpperCamelCase).\n * @example 'user_profile' -> 'UserProfile'\n * @example 'some-text' -> 'SomeText'\n */\nexport function pascalCase(str: string): string {\n if (!str) return '';\n return str\n .split(/[_\\s-]+/)\n .map((word) => {\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 RequestSuccessResponse<E> = {\n data: E;\n count?: number;\n};\n\nexport type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {\n count: number;\n};\n\nexport type RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
|
|
10
10
|
"import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * 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 JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
|
|
11
11
|
"import {\n CRUD_ROUTES,\n entityPath,\n type HttpMethod,\n type RequestCountedSuccessResponse,\n type RequestSuccessResponse,\n} from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityData,\n IdValue,\n Query,\n QueryOne,\n QueryOptions,\n QuerySearch,\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<E extends object>(\n entity: Type<E>,\n id: IdValue<E>,\n q?: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<E | undefined>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n findOne<E extends object>(\n entity: Type<E>,\n q: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n return this.read<E | undefined>(`${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`, q, opts);\n }\n\n findMany<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<E[]>> {\n const data: Query<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<E[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<E[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: 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",
|
|
12
12
|
"import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
|
|
13
13
|
],
|
|
14
|
-
"mappings": "AAEA,IAAM,EAAkC,CAAC,EAElC,SAAS,CAAM,CAAC,EAAyC,CAC9D,QAAW,KAAe,EACxB,EAAY,CAAY,EAIrB,SAAS,CAAE,CAAC,EAAiC,CAClD,EAAa,KAAK,CAAE,EACpB,IAAM,EAAQ,EAAa,OAAS,EACpC,MAAO,IAAY,CACjB,EAAa,OAAO,EAAO,CAAC,GCNzB,MAAM,UAAqB,KAAM,CAG3B,OAFX,WAAW,CACT,EACS,EACT,CACA,MAAM,CAAO,EAFJ,cAGT,KAAK,KAAO,eAEhB,CAEO,SAAS,CAAM,CAAC,EAAa,EAAuB,CACzD,OAAO,EAAW,EAAK,CAAE,OAAQ,KAAM,EAAG,CAAI,EAGzC,SAAS,CAAO,CAAC,EAAa,EAAkB,EAAuB,CAC5E,IAAM,EAAO,KAAK,UAAU,CAAO,EACnC,OAAO,EAAW,EAAK,CAAE,OAAQ,OAAQ,MAAK,EAAG,CAAI,EAGhD,SAAS,CAAQ,CAAC,EAAa,EAAkB,EAAuB,CAC7E,IAAM,EAAO,KAAK,UAAU,CAAO,EACnC,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,MAAK,EAAG,CAAI,EAGjD,SAAS,CAAM,CAAC,EAAa,EAAkB,EAAuB,CAC3E,IAAM,EAAO,KAAK,UAAU,CAAO,EACnC,OAAO,EAAW,EAAK,CAAE,OAAQ,MAAO,MAAK,EAAG,CAAI,EAG/C,SAAS,CAAS,CAAC,EAAa,EAAuB,CAC5D,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,EAAG,CAAI,EAQ5C,SAAS,CAAQ,CAAC,EAAa,EAAkB,EAAuB,CAC7E,IAAM,EAAO,KAAK,UAAU,CAAO,EACnC,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,MAAK,EAAG,CAAI,EAGxD,SAAS,CAAU,CAAC,EAAa,EAAmB,EAAuB,CAQzE,GAPA,EAAO,CAAE,MAAO,QAAS,MAAK,CAAC,EAE/B,EAAK,QAAU,CACb,OAAQ,mBACR,eAAgB,sBACb,GAAM,OACX,EACI,GAAM,OACR,EAAK,OAAS,EAAK,OAGrB,OAAO,MAAM,EAAK,CAAI,EACnB,KAAK,CAAC,IACL,EAAQ,KAAK,EAAE,KAAK,CAAC,IAAkB,CAErC,GADkB,EAAQ,QAAU,KAAO,EAAQ,OAAS,IAG1D,OADA,EAAO,CAAE,MAAO,UAAW,MAAK,CAAC,EAC1B,EAET,IAAM,EAAY,EACZ,EAAQ,CACZ,QAAS,GAAW,OAAO,SAAW,EAAQ,WAC9C,KAAM,GAAW,OAAO,MAAQ,EAAQ,MAC1C,EAEA,MADA,EAAO,CAAE,MAAO,QAAS,QAAO,MAAK,CAAC,EAChC,IAAI,EAAa,EAAM,QAAS,EAAM,IAAI,EACjD,CACH,EACC,QAAQ,IAAM,CACb,EAAO,CAAE,MAAO,WAAY,MAAK,CAAC,EACnC,
|
|
15
|
-
"debugId": "
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "BB4E96BEA0CE335F64756E2164756E21",
|
|
16
16
|
"names": []
|
|
17
17
|
}
|
|
@@ -134,6 +134,13 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
134
134
|
* for themselves and each got a different part of it wrong.
|
|
135
135
|
*/
|
|
136
136
|
readStages<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions, extra?: MongoReadStages): MongoAggregationPipelineEntry<Document>[];
|
|
137
|
+
/**
|
|
138
|
+
* `$distinct` as a `$group` on the columns the query projects - the same set a SQL dialect puts
|
|
139
|
+
* after `SELECT DISTINCT` - and the `$replaceRoot` that lifts the grouped key back to the top
|
|
140
|
+
* level. A query that projects nothing selects every column, primary key included, so there is
|
|
141
|
+
* nothing to collapse: `SELECT DISTINCT *` collapses nothing either.
|
|
142
|
+
*/
|
|
143
|
+
private distinctStages;
|
|
137
144
|
/**
|
|
138
145
|
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
139
146
|
* the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
|
|
@@ -214,6 +221,9 @@ export type MongoAggregationPipelineEntry<E extends Document> = {
|
|
|
214
221
|
$unwind?: MongoAggregationUnwind;
|
|
215
222
|
$group?: Record<string, unknown>;
|
|
216
223
|
$project?: Record<string, unknown>;
|
|
224
|
+
$replaceRoot?: {
|
|
225
|
+
readonly newRoot: string | Record<string, unknown>;
|
|
226
|
+
};
|
|
217
227
|
$addFields?: Record<string, unknown>;
|
|
218
228
|
$vectorSearch?: Record<string, unknown>;
|
|
219
229
|
$count?: string;
|
|
@@ -452,14 +452,40 @@ export class MongoDialect extends AbstractDialect {
|
|
|
452
452
|
// A `$required` relation drops parents when it unwinds, and an ordering may read a field only a
|
|
453
453
|
// lookup produces: either one puts the lookups first, as an INNER JOIN does. Otherwise paging
|
|
454
454
|
// first is equivalent and spares the lookups the rows it cuts.
|
|
455
|
+
// Merged into the query's own projection rather than standing in for one: a query that asked
|
|
456
|
+
// for no columns wants the whole document, not just the field this adds to it.
|
|
457
|
+
const projected = projection ? { ...projection, ...extra.project } : undefined;
|
|
458
|
+
const project = projected ? [{ $project: projected }] : [];
|
|
459
|
+
// `$distinct` inverts the usual order twice over: the projection decides which columns make two
|
|
460
|
+
// rows the same, so it has to run *before* the grouping, and the grouping collapses rows, so the
|
|
461
|
+
// ordering and the page have to run after it to address the set the caller actually receives.
|
|
462
|
+
const dedup = q.$distinct ? this.distinctStages(projected) : [];
|
|
463
|
+
if (dedup.length) {
|
|
464
|
+
return [...lookups, ...project, ...dedup, ...sort, ...pager];
|
|
465
|
+
}
|
|
466
|
+
// A `$required` relation drops parents when it unwinds, and an ordering may read a field only a
|
|
467
|
+
// lookup produces: either one puts the lookups first, as an INNER JOIN does. Otherwise paging
|
|
468
|
+
// first is equivalent and spares the lookups the rows it cuts.
|
|
455
469
|
const lookupsFirst = this.sortsRelations(entity, q.$sort) ||
|
|
456
470
|
lookups.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
|
|
457
|
-
return [
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
471
|
+
return [...(lookupsFirst ? [...lookups, ...sort, ...pager] : [...sort, ...pager, ...lookups]), ...project];
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* `$distinct` as a `$group` on the columns the query projects - the same set a SQL dialect puts
|
|
475
|
+
* after `SELECT DISTINCT` - and the `$replaceRoot` that lifts the grouped key back to the top
|
|
476
|
+
* level. A query that projects nothing selects every column, primary key included, so there is
|
|
477
|
+
* nothing to collapse: `SELECT DISTINCT *` collapses nothing either.
|
|
478
|
+
*/
|
|
479
|
+
distinctStages(projection) {
|
|
480
|
+
if (!projection) {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
const keys = getKeys(projection).filter((key) => projection[key] === 1);
|
|
484
|
+
if (!keys.length) {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
const groupId = Object.fromEntries(keys.map((key) => [key, `$${key}`]));
|
|
488
|
+
return [{ $group: { _id: groupId } }, { $replaceRoot: { newRoot: '$_id' } }];
|
|
463
489
|
}
|
|
464
490
|
/**
|
|
465
491
|
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
@@ -24,10 +24,11 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
24
24
|
protected internalAggregate<E extends Document, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
25
25
|
protected internalCount<E extends Document>(entity: Type<E>, qm?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
26
26
|
/**
|
|
27
|
-
* The `
|
|
28
|
-
*
|
|
27
|
+
* The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
|
|
28
|
+
* Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
|
|
29
|
+
* the floor, and skipped the relation-sort rejection {@link MongoDialect.sort} raises.
|
|
29
30
|
*/
|
|
30
|
-
private
|
|
31
|
+
private settleIds;
|
|
31
32
|
internalInsertMany<E extends Document>(entity: Type<E>, payloads: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
32
33
|
internalUpdateMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
33
34
|
private buildConflictFilter;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
2
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
3
|
-
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
|
+
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
4
|
export class MongodbQuerier extends AbstractQuerier {
|
|
5
5
|
dialect;
|
|
6
6
|
conn;
|
|
@@ -31,9 +31,10 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
31
31
|
}
|
|
32
32
|
else {
|
|
33
33
|
// A relation condition needs `$lookup`, so it forces the aggregation path just like populating
|
|
34
|
-
// one does - and so does ordering by a relation, which reads what a lookup produced
|
|
35
|
-
// `find` cursor can express none of the
|
|
36
|
-
if (
|
|
34
|
+
// one does - and so does ordering by a relation, which reads what a lookup produced, and
|
|
35
|
+
// `$distinct`, which is a `$group`. A plain `find` cursor can express none of the four.
|
|
36
|
+
if (q.$distinct ||
|
|
37
|
+
populatesRelations(meta, q.$populate) ||
|
|
37
38
|
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
38
39
|
this.dialect.sortsRelations(entity, q.$sort)) {
|
|
39
40
|
const pipeline = this.dialect.aggregationPipeline(entity, q, opts);
|
|
@@ -143,15 +144,14 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
/**
|
|
146
|
-
* The `
|
|
147
|
-
*
|
|
147
|
+
* The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
|
|
148
|
+
* Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
|
|
149
|
+
* the floor, and skipped the relation-sort rejection {@link MongoDialect.sort} raises.
|
|
148
150
|
*/
|
|
149
|
-
async
|
|
151
|
+
async settleIds(entity, q, opts) {
|
|
150
152
|
const meta = getMeta(entity);
|
|
151
|
-
const
|
|
152
|
-
const founds = await this.execute((session) => this.collection(entity)
|
|
153
|
-
.aggregate([...stages, { $match: filter }, { $project: { _id: true } }], { session })
|
|
154
|
-
.toArray());
|
|
153
|
+
const pipeline = this.dialect.aggregationPipeline(entity, idOnlyQuery(meta, q), opts);
|
|
154
|
+
const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
155
155
|
return (this.dialect.normalizeIds(meta, founds) || []).map((found) => found[meta.id]);
|
|
156
156
|
}
|
|
157
157
|
async internalInsertMany(entity, payloads) {
|
|
@@ -176,9 +176,11 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
176
176
|
payload = clone(payload);
|
|
177
177
|
const meta = getMeta(entity);
|
|
178
178
|
const persistable = this.dialect.getPersistable(meta, payload, 'onUpdate');
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
// Settled to ids first in two cases: an `updateMany` filter cannot host a `$lookup`, so a
|
|
180
|
+
// relation condition has nowhere to go, and MongoDB takes no page on a write, so a paged one
|
|
181
|
+
// has to name the rows it picked rather than touching every match.
|
|
182
|
+
const where = this.dialect.constrainsRelations(entity, qm.$where) || isPagedQuery(qm)
|
|
183
|
+
? { _id: { $in: await this.settleIds(entity, qm, opts) } }
|
|
182
184
|
: this.dialect.where(entity, qm.$where, opts);
|
|
183
185
|
// Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
|
|
184
186
|
const update = this.dialect.getUpdateFilter(persistable);
|
|
@@ -252,8 +254,8 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
252
254
|
// Hard delete targets matching rows regardless of soft-delete state (keeps other filters).
|
|
253
255
|
const findOpts = field ? opts : { ...opts, filters: withoutSoftDeleteFilter(opts.filters) };
|
|
254
256
|
// Delete has always resolved its ids first (it stamps or removes them by `_id`), so a relation
|
|
255
|
-
// condition needs nothing extra here.
|
|
256
|
-
const ids = await this.
|
|
257
|
+
// condition needs nothing extra here - and passing the whole query is what makes its page apply.
|
|
258
|
+
const ids = await this.settleIds(entity, qm, findOpts);
|
|
257
259
|
if (!ids.length) {
|
|
258
260
|
return 0;
|
|
259
261
|
}
|
|
@@ -30,6 +30,11 @@ export type UqlModuleAsyncOptions<Req = unknown> = UqlModuleCommon<Req> & {
|
|
|
30
30
|
/** Providers to inject into `useFactory`. */
|
|
31
31
|
readonly inject?: FactoryProvider['inject'];
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
|
|
35
|
+
* `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
|
|
36
|
+
* every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
|
|
37
|
+
*/
|
|
33
38
|
export declare class UqlModule {
|
|
34
39
|
/** Configure with an already-built pool. */
|
|
35
40
|
static forRoot<Req = unknown>({ pool, global, getContext }: UqlModuleOptions<Req>): DynamicModule;
|
package/dist/nestjs/uqlModule.js
CHANGED
|
@@ -43,11 +43,6 @@ import { UqlContextInterceptor } from './uqlContextInterceptor.js';
|
|
|
43
43
|
* does not redirect UQL internals.
|
|
44
44
|
*/
|
|
45
45
|
export const UQL_QUERIER_POOL = Symbol('UQL_QUERIER_POOL');
|
|
46
|
-
/**
|
|
47
|
-
* NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
|
|
48
|
-
* `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
|
|
49
|
-
* every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
|
|
50
|
-
*/
|
|
51
46
|
/**
|
|
52
47
|
* Ends the pool when Nest shuts down.
|
|
53
48
|
*
|
|
@@ -66,6 +61,11 @@ class UqlPoolLifecycle {
|
|
|
66
61
|
return this.pool.end();
|
|
67
62
|
}
|
|
68
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
|
|
66
|
+
* `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
|
|
67
|
+
* every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
|
|
68
|
+
*/
|
|
69
69
|
let UqlModule = (() => {
|
|
70
70
|
let _classDecorators = [Module({})];
|
|
71
71
|
let _classDescriptor;
|
|
@@ -23,11 +23,13 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
23
23
|
private validateProjectionQueryRecursive;
|
|
24
24
|
/**
|
|
25
25
|
* Resolves `[entity, query, opts]` for the dual call pattern: `(entity, q, opts)` (entity argument)
|
|
26
|
-
* vs `(query, opts)` (entity via the query's `$entity` field).
|
|
26
|
+
* vs `(query, opts)` (entity via the query's `$entity` field). Generic in the query `Q` because it
|
|
27
|
+
* only ever reads `$entity`: pinning it to one statement's shape made every caller launder its own
|
|
28
|
+
* through a cast, which is how a read query's `$sort` used to reach a write's.
|
|
27
29
|
*/
|
|
28
|
-
protected resolveEntityQuery<E extends object>(entityOrQuery: Type<E> | (
|
|
30
|
+
protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
|
|
29
31
|
$entity: Type<E>;
|
|
30
|
-
}), maybeQueryOrOpts?:
|
|
32
|
+
}), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
|
|
31
33
|
findOneById<E extends object>(entity: Type<E>, id: IdValue<E>, q?: QueryOne<E>, opts?: QueryOptions): Promise<E | undefined>;
|
|
32
34
|
/**
|
|
33
35
|
* Find a single record matching the query.
|
|
@@ -123,7 +125,12 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
123
125
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
124
126
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
125
127
|
protected deleteRelations<E extends object>(entity: Type<E>, ids: IdValue<E>[], opts?: QueryOptions): Promise<void>;
|
|
126
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Persists `relValue` against every id in `ids`, which an update hands the whole page of rows it
|
|
130
|
+
* settled: the value is the same for all of them, so the statements are per relation rather than per
|
|
131
|
+
* row wherever the cardinality allows it.
|
|
132
|
+
*/
|
|
133
|
+
protected saveRelation<E extends object>(entity: Type<E>, ids: IdValue<E>[], relValue: unknown, relKey: RelationKey<E>, isUpdate?: boolean): Promise<void>;
|
|
127
134
|
private saveToMany;
|
|
128
135
|
private saveOneToOne;
|
|
129
136
|
private saveManyToOne;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
/**
|
|
5
5
|
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
@@ -65,7 +65,9 @@ export class AbstractQuerier {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Resolves `[entity, query, opts]` for the dual call pattern: `(entity, q, opts)` (entity argument)
|
|
68
|
-
* vs `(query, opts)` (entity via the query's `$entity` field).
|
|
68
|
+
* vs `(query, opts)` (entity via the query's `$entity` field). Generic in the query `Q` because it
|
|
69
|
+
* only ever reads `$entity`: pinning it to one statement's shape made every caller launder its own
|
|
70
|
+
* through a cast, which is how a read query's `$sort` used to reach a write's.
|
|
69
71
|
*/
|
|
70
72
|
resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
71
73
|
if (typeof entityOrQuery === 'function' && entityOrQuery.prototype) {
|
|
@@ -312,7 +314,7 @@ export class AbstractQuerier {
|
|
|
312
314
|
}, []);
|
|
313
315
|
if (!entries.length)
|
|
314
316
|
return;
|
|
315
|
-
await Promise.all(entries.map(({ it, relKeys }) => Promise.all(relKeys.map((relKey) => this.saveRelation(entity, it, relKey)))));
|
|
317
|
+
await Promise.all(entries.map(({ it, relKeys }) => Promise.all(relKeys.map((relKey) => this.saveRelation(entity, [it[meta.id]], it[relKey], relKey)))));
|
|
316
318
|
}
|
|
317
319
|
async updateRelations(entity, q, payload, opts) {
|
|
318
320
|
const meta = getMeta(entity);
|
|
@@ -320,9 +322,14 @@ export class AbstractQuerier {
|
|
|
320
322
|
if (!relKeys.length) {
|
|
321
323
|
return;
|
|
322
324
|
}
|
|
323
|
-
const founds = await this.findMany(entity,
|
|
325
|
+
const founds = await this.findMany(entity, idOnlyQuery(meta, q), opts);
|
|
324
326
|
const ids = founds.map((found) => found[meta.id]);
|
|
325
|
-
|
|
327
|
+
if (!ids.length) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
for (const relKey of relKeys) {
|
|
331
|
+
await this.saveRelation(entity, ids, payload[relKey], relKey, true);
|
|
332
|
+
}
|
|
326
333
|
}
|
|
327
334
|
async deleteRelations(entity, ids, opts) {
|
|
328
335
|
const meta = getMeta(entity);
|
|
@@ -344,66 +351,70 @@ export class AbstractQuerier {
|
|
|
344
351
|
}
|
|
345
352
|
}
|
|
346
353
|
}
|
|
347
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Persists `relValue` against every id in `ids`, which an update hands the whole page of rows it
|
|
356
|
+
* settled: the value is the same for all of them, so the statements are per relation rather than per
|
|
357
|
+
* row wherever the cardinality allows it.
|
|
358
|
+
*/
|
|
359
|
+
async saveRelation(entity, ids, relValue, relKey, isUpdate) {
|
|
348
360
|
const meta = getMeta(entity);
|
|
349
|
-
const id = payload[meta.id];
|
|
350
361
|
const relOpts = meta.relations[relKey];
|
|
351
362
|
if (!relOpts)
|
|
352
363
|
return;
|
|
353
364
|
const relEntity = relOpts.entity();
|
|
354
|
-
const relPayload =
|
|
365
|
+
const relPayload = relValue;
|
|
355
366
|
switch (relOpts.cardinality) {
|
|
356
367
|
case '1m':
|
|
357
368
|
case 'mm':
|
|
358
|
-
return this.saveToMany(relOpts, relEntity,
|
|
369
|
+
return this.saveToMany(relOpts, relEntity, ids, relPayload, isUpdate);
|
|
359
370
|
case '11':
|
|
360
|
-
return this.saveOneToOne(relEntity, relOpts,
|
|
371
|
+
return this.saveOneToOne(relEntity, relOpts, ids, relPayload);
|
|
361
372
|
case 'm1':
|
|
362
373
|
if (relPayload)
|
|
363
|
-
return this.saveManyToOne(entity, relEntity, relOpts,
|
|
374
|
+
return this.saveManyToOne(entity, relEntity, relOpts, ids, relPayload);
|
|
364
375
|
}
|
|
365
376
|
}
|
|
366
|
-
async saveToMany(relOpts, relEntity,
|
|
377
|
+
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
367
378
|
const { references, through } = relOpts;
|
|
368
379
|
if (through) {
|
|
369
380
|
const localField = references[0].local;
|
|
370
381
|
const throughEntity = through();
|
|
371
382
|
if (isUpdate) {
|
|
372
|
-
await this.deleteMany(throughEntity, { $where: { [localField]:
|
|
383
|
+
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
373
384
|
}
|
|
374
385
|
if (relPayload) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
386
|
+
// Saved per parent on purpose: each one owns its copies of the children, and saving them once
|
|
387
|
+
// would link every parent to a single shared row instead.
|
|
388
|
+
for (const id of ids) {
|
|
389
|
+
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
390
|
+
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [references[0].local]: id, [references[1].local]: relId })));
|
|
391
|
+
}
|
|
381
392
|
}
|
|
382
393
|
return;
|
|
383
394
|
}
|
|
384
395
|
const foreignField = references[0].foreign;
|
|
385
396
|
if (isUpdate) {
|
|
386
|
-
await this.deleteMany(relEntity, { $where: { [foreignField]:
|
|
397
|
+
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
387
398
|
}
|
|
388
399
|
if (relPayload) {
|
|
389
|
-
|
|
390
|
-
it[foreignField] = id;
|
|
391
|
-
}
|
|
392
|
-
await this.saveMany(relEntity, relPayload);
|
|
400
|
+
await this.saveMany(relEntity, ids.flatMap((id) => relPayload.map((it) => ({ ...it, [foreignField]: id }))));
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
|
-
async saveOneToOne(relEntity, relOpts,
|
|
403
|
+
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
396
404
|
const foreignField = relOpts.references[0].foreign;
|
|
397
405
|
if (relPayload === null) {
|
|
398
|
-
await this.deleteMany(relEntity, { $where: { [foreignField]:
|
|
406
|
+
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
399
407
|
return;
|
|
400
408
|
}
|
|
401
|
-
await this.
|
|
409
|
+
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
402
410
|
}
|
|
403
|
-
async saveManyToOne(entity, relEntity, relOpts,
|
|
411
|
+
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
404
412
|
const localField = relOpts.references[0].local;
|
|
405
|
-
|
|
406
|
-
|
|
413
|
+
// Per parent: each gets its own reference row, so each `SET` carries a different value.
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
const referenceId = await this.insertOne(relEntity, relPayload);
|
|
416
|
+
await this.updateOneById(entity, id, { [localField]: referenceId });
|
|
417
|
+
}
|
|
407
418
|
}
|
|
408
419
|
/**
|
|
409
420
|
* Runs `callback` in a transaction: begin, commit on success, roll back on failure.
|
|
@@ -58,8 +58,10 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
58
58
|
* populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
|
|
59
59
|
* `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
|
|
60
60
|
*
|
|
61
|
-
* `visited` guards a populated graph that points back at itself
|
|
62
|
-
* a
|
|
61
|
+
* `visited` guards a populated graph that points back at itself, and makes a node two paths reach
|
|
62
|
+
* decode once. Only a relation can lead the walk back somewhere it has been, so an entity that
|
|
63
|
+
* declares none skips the guard rather than allocating a set per row to hold a single object -
|
|
64
|
+
* which cost more than the decoding it guards, on a flat read.
|
|
63
65
|
*/
|
|
64
66
|
private hydrateFields;
|
|
65
67
|
protected internalCount<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
2
2
|
import { getMeta } from '../entity/index.js';
|
|
3
|
-
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, isAutoIncrement, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
|
+
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
5
|
import { enrichError } from './queryError.js';
|
|
6
|
-
/**
|
|
7
|
-
* Whether `q` picks a specific slice of the matching rows rather than all of them. A write that
|
|
8
|
-
* does has to settle those rows and name them: `ORDER BY`/`LIMIT` on an UPDATE or DELETE is MySQL's
|
|
9
|
-
* alone.
|
|
10
|
-
*/
|
|
11
|
-
function isPaged(q) {
|
|
12
|
-
return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
|
|
13
|
-
}
|
|
14
6
|
export class AbstractSqlQuerier extends AbstractQuerier {
|
|
15
7
|
dialect;
|
|
16
8
|
extra;
|
|
@@ -130,14 +122,15 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
130
122
|
* populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
|
|
131
123
|
* `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
|
|
132
124
|
*
|
|
133
|
-
* `visited` guards a populated graph that points back at itself
|
|
134
|
-
* a
|
|
125
|
+
* `visited` guards a populated graph that points back at itself, and makes a node two paths reach
|
|
126
|
+
* decode once. Only a relation can lead the walk back somewhere it has been, so an entity that
|
|
127
|
+
* declares none skips the guard rather than allocating a set per row to hold a single object -
|
|
128
|
+
* which cost more than the decoding it guards, on a flat read.
|
|
135
129
|
*/
|
|
136
|
-
hydrateFields(entity, dto, visited
|
|
137
|
-
if (!dto || typeof dto !== 'object' || visited
|
|
130
|
+
hydrateFields(entity, dto, visited) {
|
|
131
|
+
if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
|
|
138
132
|
return dto;
|
|
139
133
|
}
|
|
140
|
-
visited.add(dto);
|
|
141
134
|
const meta = getMeta(entity);
|
|
142
135
|
const row = dto;
|
|
143
136
|
for (const [key, kind] of this.dialect.hydratableFields(entity)) {
|
|
@@ -146,21 +139,30 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
146
139
|
row[key] = decodeColumn(value, kind);
|
|
147
140
|
}
|
|
148
141
|
}
|
|
142
|
+
// Allocated only where the walk can continue: an entity declaring no relation cannot lead back
|
|
143
|
+
// to a node already decoded, and the loop below is a no-op for it anyway.
|
|
144
|
+
if (hasKeys(meta.relations)) {
|
|
145
|
+
visited ??= new WeakSet();
|
|
146
|
+
}
|
|
147
|
+
visited?.add(dto);
|
|
148
|
+
// The value is read before the relation's target is resolved: a query that populated nothing
|
|
149
|
+
// still walks every relation the entity declares, and `rel.entity()` is a call per row per
|
|
150
|
+
// relation that only the populated ones need.
|
|
149
151
|
for (const key in meta.relations) {
|
|
152
|
+
const value = row[key];
|
|
153
|
+
if (!value || typeof value !== 'object')
|
|
154
|
+
continue;
|
|
150
155
|
const rel = meta.relations[key];
|
|
151
156
|
if (!rel)
|
|
152
157
|
continue;
|
|
153
158
|
const relEntity = rel.entity();
|
|
154
|
-
const value = row[key];
|
|
155
159
|
if (Array.isArray(value)) {
|
|
156
160
|
for (const it of value) {
|
|
157
161
|
this.hydrateFields(relEntity, it, visited);
|
|
158
162
|
}
|
|
159
163
|
continue;
|
|
160
164
|
}
|
|
161
|
-
|
|
162
|
-
this.hydrateFields(relEntity, value, visited);
|
|
163
|
-
}
|
|
165
|
+
this.hydrateFields(relEntity, value, visited);
|
|
164
166
|
}
|
|
165
167
|
return dto;
|
|
166
168
|
}
|
|
@@ -230,7 +232,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
230
232
|
// Settled first for the reason `internalDeleteMany` settles: `ORDER BY`/`LIMIT` on an UPDATE is
|
|
231
233
|
// MySQL's alone, so a paged update has to name the rows it picked.
|
|
232
234
|
let target = q;
|
|
233
|
-
if (
|
|
235
|
+
if (isPagedQuery(q)) {
|
|
234
236
|
const ids = await this.settleIds(entity, q, opts);
|
|
235
237
|
if (!ids.length) {
|
|
236
238
|
return 0;
|
|
@@ -247,7 +249,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
247
249
|
async settleIds(entity, q, opts) {
|
|
248
250
|
const meta = getMeta(entity);
|
|
249
251
|
const ctx = this.dialect.createContext();
|
|
250
|
-
this.dialect.find(ctx, entity,
|
|
252
|
+
this.dialect.find(ctx, entity, idOnlyQuery(meta, q), opts);
|
|
251
253
|
const founds = await this.all(ctx.sql, ctx.values);
|
|
252
254
|
return founds.map((it) => it[meta.id]);
|
|
253
255
|
}
|
|
@@ -278,7 +280,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
278
280
|
// parents' ids to find their children, and no engine but MySQL accepts `ORDER BY`/`LIMIT` on a
|
|
279
281
|
// DELETE, so a paged delete has to name the rows it settled on. A plain predicate needs neither,
|
|
280
282
|
// and there the round trip buys nothing: the statement can say what the caller already said.
|
|
281
|
-
if (!
|
|
283
|
+
if (!isPagedQuery(q) && !cascadesOnDelete(meta)) {
|
|
282
284
|
const ctx = this.dialect.createContext();
|
|
283
285
|
this.dialect.delete(ctx, entity, q, opts);
|
|
284
286
|
const { changes = 0 } = await this.run(ctx.sql, ctx.values);
|
package/dist/type/query.d.ts
CHANGED
|
@@ -36,7 +36,9 @@ export type QuerySelectOptions = {
|
|
|
36
36
|
autoPrefixAlias?: boolean;
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
|
-
* Query field selection - `{ name: true }` whitelists specific fields.
|
|
39
|
+
* Query field selection - `{ name: true }` whitelists specific fields. Fields only: a relation is a
|
|
40
|
+
* sub-query rather than a projection flag, and a whitelist naming one could not say whether the
|
|
41
|
+
* scalars come with it. Relations go in `$populate`.
|
|
40
42
|
*/
|
|
41
43
|
export type QuerySelect<E> = {
|
|
42
44
|
[K in FieldKey<E>]?: BooleanLike;
|
|
@@ -116,17 +118,27 @@ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
|
|
|
116
118
|
* Accepted value for a field in `$sort` - either a direction or a vector similarity search.
|
|
117
119
|
*/
|
|
118
120
|
export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
|
|
121
|
+
/**
|
|
122
|
+
* To-one relations only: a parent holds many rows of a to-many, so there is no single value to order
|
|
123
|
+
* it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
|
|
124
|
+
*/
|
|
125
|
+
type ToOneRelationKey<E> = {
|
|
126
|
+
[K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
|
|
127
|
+
}[RelationKey<E>];
|
|
119
128
|
/**
|
|
120
129
|
* sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,
|
|
121
130
|
* like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on
|
|
122
|
-
* `number[]` fields.
|
|
131
|
+
* `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:
|
|
132
|
+
* the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank
|
|
133
|
+
* there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.
|
|
134
|
+
*
|
|
135
|
+
* One mapped type over the three key sets rather than three intersected. The sets are disjoint - a
|
|
136
|
+
* JSON path is dotted, and a field key cannot also be a relation key - and an assignability check
|
|
137
|
+
* against an intersection is repeated per constituent, which made this the single most expensive
|
|
138
|
+
* type in the package to check.
|
|
123
139
|
*/
|
|
124
140
|
export type QuerySortMap<E, Vector extends boolean = true> = {
|
|
125
|
-
[K in FieldKey<E>]?: Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
|
|
126
|
-
} & {
|
|
127
|
-
[P in JsonFieldPaths<E>]?: QuerySortDirection;
|
|
128
|
-
} & {
|
|
129
|
-
[K in RelationKey<E> as IsMany<E[K]> extends true ? never : K]?: QuerySortMap<RelationTarget<E[K]>, false>;
|
|
141
|
+
[K in FieldKey<E> | JsonFieldPaths<E> | ToOneRelationKey<E>]?: K extends RelationKey<E> ? QuerySortMap<RelationTarget<E[K]>, false> : K extends FieldKey<E> ? Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection : QuerySortDirection;
|
|
130
142
|
};
|
|
131
143
|
/**
|
|
132
144
|
* pager options.
|
|
@@ -153,17 +165,16 @@ export type QueryFilter<E> = {
|
|
|
153
165
|
};
|
|
154
166
|
/**
|
|
155
167
|
* A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the rows they
|
|
156
|
-
* picked before writing, so the page is portable rather than MySQL-only
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* list to hold one. Passing `QuerySortMap` its `Vector = false` is what keeps it off these.
|
|
168
|
+
* picked with a SELECT before writing, so the page is portable rather than MySQL-only - and so a
|
|
169
|
+
* vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the
|
|
170
|
+
* projection list to hold the distance. `$lock` is the clause that stays off these, declared on
|
|
171
|
+
* {@link Query} instead.
|
|
161
172
|
*/
|
|
162
173
|
export type QuerySearch<E> = QueryFilter<E> & {
|
|
163
174
|
/**
|
|
164
175
|
* sorting options.
|
|
165
176
|
*/
|
|
166
|
-
$sort?: QuerySortMap<E
|
|
177
|
+
$sort?: QuerySortMap<E>;
|
|
167
178
|
} & QueryPager;
|
|
168
179
|
/**
|
|
169
180
|
* query options.
|
|
@@ -243,3 +254,4 @@ export type QueryUpdateResult = {
|
|
|
243
254
|
*/
|
|
244
255
|
created?: boolean;
|
|
245
256
|
};
|
|
257
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
|
|
1
|
+
import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
|
|
2
2
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
3
3
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
4
4
|
/**
|
|
@@ -41,6 +41,19 @@ export declare function filterPersistableRelationKeys<E>(meta: EntityMeta<E>, pa
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function cascadesOnDelete<E>(meta: EntityMeta<E>): boolean;
|
|
43
43
|
export declare function isCascadable(action: CascadeType, configuration?: boolean | CascadeType): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether `q` carries an ordering or a page, so a write has to settle its rows with a read and name
|
|
46
|
+
* them by id. `$sort` counts even without a page: the SQL dialects emit it from the same `search()`
|
|
47
|
+
* that `find` uses, and SQLite rejects `ORDER BY` on an UPDATE that has no `LIMIT`. MongoDB takes
|
|
48
|
+
* neither clause on a write at all.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isPagedQuery<E>(q: QuerySearch<E>): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
|
|
53
|
+
* rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
|
|
54
|
+
* compiler - so it is spelled once here rather than in each querier.
|
|
55
|
+
*/
|
|
56
|
+
export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
|
|
44
57
|
/**
|
|
45
58
|
* The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
|
|
46
59
|
* narrowing cast: `Array.isArray` does not narrow `readonly` arrays out of a union.
|
|
@@ -96,6 +96,23 @@ export function isCascadable(action, configuration) {
|
|
|
96
96
|
}
|
|
97
97
|
return configuration === action;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether `q` carries an ordering or a page, so a write has to settle its rows with a read and name
|
|
101
|
+
* them by id. `$sort` counts even without a page: the SQL dialects emit it from the same `search()`
|
|
102
|
+
* that `find` uses, and SQLite rejects `ORDER BY` on an UPDATE that has no `LIMIT`. MongoDB takes
|
|
103
|
+
* neither clause on a write at all.
|
|
104
|
+
*/
|
|
105
|
+
export function isPagedQuery(q) {
|
|
106
|
+
return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
|
|
110
|
+
* rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
|
|
111
|
+
* compiler - so it is spelled once here rather than in each querier.
|
|
112
|
+
*/
|
|
113
|
+
export function idOnlyQuery(meta, q) {
|
|
114
|
+
return { ...q, $select: { [meta.id]: true } };
|
|
115
|
+
}
|
|
99
116
|
/**
|
|
100
117
|
* The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
|
|
101
118
|
* narrowing cast: `Array.isArray` does not narrow `readonly` arrays out of a union.
|
|
@@ -4,13 +4,11 @@ export declare function throwNoPendingTransaction(): never;
|
|
|
4
4
|
export declare function clone<T>(value: T): T;
|
|
5
5
|
/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */
|
|
6
6
|
export declare function hasKeys<T>(obj: T): obj is NonNullable<T>;
|
|
7
|
-
/** Whether `obj` has at least two enumerable keys. */
|
|
8
|
-
export declare function hasMultipleKeys(obj: object): boolean;
|
|
9
7
|
/**
|
|
10
8
|
* Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match
|
|
11
9
|
* without materializing a key array (unlike `Object.keys(obj).some(pred)`).
|
|
12
10
|
*/
|
|
13
|
-
export declare function someKey(obj:
|
|
11
|
+
export declare function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean;
|
|
14
12
|
/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */
|
|
15
13
|
export declare function someValue(obj: object, pred: (value: unknown) => boolean): boolean;
|
|
16
14
|
/**
|
package/dist/util/object.util.js
CHANGED
|
@@ -21,15 +21,6 @@ export function hasKeys(obj) {
|
|
|
21
21
|
return true;
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
-
/** Whether `obj` has at least two enumerable keys. */
|
|
25
|
-
export function hasMultipleKeys(obj) {
|
|
26
|
-
let count = 0;
|
|
27
|
-
for (const _ in obj) {
|
|
28
|
-
if (++count > 1)
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
24
|
/**
|
|
34
25
|
* Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match
|
|
35
26
|
* without materializing a key array (unlike `Object.keys(obj).some(pred)`).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getKeys } from './object.util.js';
|
|
1
|
+
import { getKeys, someKey } from './object.util.js';
|
|
2
2
|
/**
|
|
3
3
|
* Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
|
|
4
4
|
* the one field it reads, so it answers for a relation being declared as well as for a resolved one.
|
|
@@ -58,7 +58,7 @@ export function getRelationRequestSummary(meta, populate) {
|
|
|
58
58
|
export function populatesRelations(meta, populate) {
|
|
59
59
|
if (!populate)
|
|
60
60
|
return false;
|
|
61
|
-
return
|
|
61
|
+
return someKey(populate, (key) => !!populate[key] && key in meta.relations);
|
|
62
62
|
}
|
|
63
63
|
// Keep in sync with `Query`'s own keys (`type/query.ts`); `$where`'s value type is `QueryWhere` (`type/queryWhere.ts`).
|
|
64
64
|
const RELATION_QUERY_BOOLEAN_KEYS = new Set(['$distinct', '$required']);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native TypeScript 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.31.
|
|
6
|
+
"version": "0.31.4",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|