uql-orm 0.31.3 → 0.31.5
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 +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +6 -5
- package/dist/dialect/abstractSqlDialect.d.ts +1 -1
- package/dist/dialect/abstractSqlDialect.js +2 -2
- package/dist/http/query.js +21 -13
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +1 -1
- package/dist/migrate/migrator.js +4 -4
- package/dist/migrate/schemaGenerator.js +2 -2
- package/dist/mongo/mongoDialect.d.ts +10 -0
- package/dist/mongo/mongoDialect.js +70 -18
- package/dist/mongo/mongodbQuerier.d.ts +4 -3
- package/dist/mongo/mongodbQuerier.js +24 -16
- package/dist/querier/abstractQuerier.d.ts +6 -1
- package/dist/querier/abstractQuerier.js +39 -30
- package/dist/querier/abstractSqlQuerier.d.ts +4 -2
- package/dist/querier/abstractSqlQuerier.js +23 -21
- package/dist/sqlite/sqliteQuerierPool.js +2 -1
- package/dist/type/query.d.ts +25 -1
- package/dist/type/query.js +18 -1
- package/dist/type/queryAggregate.d.ts +3 -4
- package/dist/type/queryWhere.d.ts +7 -4
- 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 +9 -10
- 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
|
|
|
@@ -55,7 +55,7 @@ The query is just JSON: build it dynamically, store it, diff it, or send it from
|
|
|
55
55
|
- **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [virtual fields](https://uql-orm.dev/entities/virtual-fields) are sub-queries you can filter on, and a migration can be plain SQL.
|
|
56
56
|
- **Light.** Zero runtime dependencies, under 280 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
57
57
|
- **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
|
|
58
|
-
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), by
|
|
58
|
+
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), by 2.6-3x over the next closest and roughly 10x over the slowest, on Bun, Node and Deno alike. The same benchmark [scores the types](https://github.com/rogerpadilla/ts-orm-benchmark#type-safety) by writing ten ordinary mistakes in six ORMs' APIs and compiling them; UQL ties for first at 9 of 10, and the one it misses is there in the table too.
|
|
59
59
|
|
|
60
60
|
## Get started
|
|
61
61
|
|
|
@@ -70,7 +70,7 @@ Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/ma
|
|
|
70
70
|
|
|
71
71
|
## Made with UQL
|
|
72
72
|
|
|
73
|
-
**[Variability.ai](https://variability.ai)** - AI meeting
|
|
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
74
|
|
|
75
75
|
Built something? [Open a PR](https://github.com/rogerpadilla/uql/blob/main/CONTRIBUTING.md) and add it here.
|
|
76
76
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var M=[];function W(I){for(let X of M)X(I)}function _(I){M.push(I);let X=M.length-1;return()=>{M.splice(X,1)}}class L extends Error{status;constructor(I,X){super(I);this.status=X;this.name="RequestError"}}function N(I,X){return F(I,{method:"get"},X)}function x(I,X,V){let Z=JSON.stringify(X);return F(I,{method:"post",body:Z},V)}function Q(I,X,V){let Z=JSON.stringify(X);return F(I,{method:"patch",body:Z},V)}function A(I,X,V){let Z=JSON.stringify(X);return F(I,{method:"put",body:Z},V)}function K(I,X){return F(I,{method:"delete"},X)}function T(I,X,V){let Z=JSON.stringify(X);return F(I,{method:"QUERY",body:Z},V)}function F(I,X,V){if(W({phase:"start",opts:V}),X.headers={accept:"application/json","content-type":"application/json",...V?.headers},V?.signal)X.signal=V.signal;return fetch(I,X).then((Z)=>Z.json().then(($)=>{if(Z.status>=200&&Z.status<300)return W({phase:"success",opts:V}),$;let f=$,J={message:f?.error?.message??Z.statusText,code:f?.error?.code??Z.status};throw W({phase:"error",error:J,opts:V}),new L(J.message,J.code)})).finally(()=>{W({phase:"complete",opts:V})})}function B(I){return I?Object.keys(I):[]}function C(I){let X=I.charAt(0).toLowerCase();for(let V=1;V<I.length;++V)X+=I[V]===I[V].toUpperCase()?"-"+I[V].toLowerCase():I[V];return X}var G={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:""}},j=B(G),v=new Map(j.filter((I)=>G[I].method==="GET"&&G[I].path!=="/:id").map((I)=>[G[I].path,I]));function D(I){return C(I.name)}var P=["$select","$populate","$exclude","$where","$sort"],c=["$skip","$limit"],g=["$distinct"];var u=new Set([...P,...c,...g,"hardDelete","count"]);function H(I){if(!I)return"";let X=new URLSearchParams;for(let Z of B(I)){let $=I[Z];if($===void 0)continue;X.append(Z,typeof $==="object"&&$!==null?JSON.stringify($):String($))}let V=X.toString();return V?`?${V}`:""}class Y{basePath;defaults;constructor(I,X={}){this.basePath=I;this.defaults=X}findOneById(I,X,V,Z){let $=this.getBasePath(I),z=H(V);return N(`${$}/${X}${z}`,this.buildOptions(Z))}findOne(I,X,V){return this.read(`${this.getBasePath(I)}${G.findOne.path}`,X,V)}findMany(I,X,V){let Z={...X};if(V?.count)Z.count=!0;return this.read(this.getBasePath(I),Z,V)}async findManyAndCount(I,X,V){let Z=await this.findMany(I,X,{...V,count:!0});if(typeof Z.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...Z,count:Z.count}}count(I,X,V){return this.read(`${this.getBasePath(I)}${G.count.path}`,X,V)}insertOne(I,X,V){let Z=this.getBasePath(I);return x(Z,X,this.buildOptions(V))}insertMany(I,X,V){let Z=this.getBasePath(I);return x(`${Z}${G.insertMany.path}`,X,this.buildOptions(V))}updateOneById(I,X,V,Z){let $=this.getBasePath(I);return Q(`${$}/${X}`,V,this.buildOptions(Z))}updateMany(I,X,V,Z){let $=this.getBasePath(I),z=H(X);return Q(`${$}${z}`,V,this.buildOptions(Z))}saveOne(I,X,V){let Z=this.getBasePath(I);return A(Z,X,this.buildOptions(V))}saveMany(I,X,V){let Z=this.getBasePath(I);return A(`${Z}${G.saveMany.path}`,X,this.buildOptions(V))}deleteOneById(I,X,V={}){let Z=this.getBasePath(I),$=V.hardDelete?H({hardDelete:V.hardDelete}):"";return K(`${Z}/${X}${$}`,this.buildOptions(V))}deleteMany(I,X,V={}){let Z=this.getBasePath(I),$=H(V.hardDelete?{...X,hardDelete:V.hardDelete}:X);return K(`${Z}${$}`,this.buildOptions(V))}getBasePath(I){return`${this.basePath}/${D(I)}`}read(I,X,V){if(this.defaults.readMethod==="QUERY")return T(I,X??{},this.buildOptions(V));return N(`${I}${H(X)}`,this.buildOptions(V))}buildOptions(I){if(!this.defaults.headers&&!I?.headers)return I;return{...I,headers:{...this.defaults.headers,...I?.headers}}}}var k={getQuerier:()=>new Y("/api")};function s(I){k=I}function O(){return k}function a(){return O().getQuerier()}export{s as setQuerierPool,K as remove,T as query,A as put,x as post,Q as patch,_ as on,W as notify,O as getQuerierPool,a as getQuerier,N as get,L as RequestError,Y as HttpQuerier};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=7FE2CA42A2875AC664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=uql-browser.min.js.map
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/browser/http/bus.ts", "../../src/browser/http/http.ts", "../../src/util/object.util.ts", "../../src/util/string.util.ts", "../../src/http/contract.ts", "../../src/http/query.ts", "../../src/browser/querier/httpQuerier.ts", "../../src/browser/options.ts"],
|
|
3
|
+
"sources": ["../../src/browser/http/bus.ts", "../../src/browser/http/http.ts", "../../src/util/object.util.ts", "../../src/util/string.util.ts", "../../src/http/contract.ts", "../../src/type/query.ts", "../../src/http/query.ts", "../../src/browser/querier/httpQuerier.ts", "../../src/browser/options.ts"],
|
|
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
|
-
"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",
|
|
10
|
+
"import type { FieldKey, 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 * 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/**\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 */\nexport type QuerySortMap<E, Vector extends boolean = true> = {\n [K in FieldKey<E> | JsonFieldPaths<E> | ToOneRelationKey<E>]?: K extends RelationKey<E>\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. `count` takes exactly this: how many rows match is all a count\n * can answer, so an ordering or a page on it is a clause it could only drop or choke on.\n */\nexport type QueryFilter<E> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\n};\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the rows they\n * picked with a SELECT before writing, so the page is portable rather than MySQL-only - and so 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` is the clause that stays off these, declared on\n * {@link Query} instead.\n */\nexport type QuerySearch<E> = QueryFilter<E> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n} & QueryPager;\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 * 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 // `$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\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] 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 * 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
|
+
"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 { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } 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_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) {\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) {\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",
|
|
11
12
|
"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
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"
|
|
13
14
|
],
|
|
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": "
|
|
15
|
+
"mappings": "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",
|
|
16
|
+
"debugId": "7FE2CA42A2875AC664756E2164756E21",
|
|
16
17
|
"names": []
|
|
17
18
|
}
|
|
@@ -246,7 +246,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
246
246
|
* `INSERT INTO ... VALUES (...)` and nothing more. The upsert builders extend this rather than
|
|
247
247
|
* {@link insert}: their own clause has to come before the `RETURNING`, not after it.
|
|
248
248
|
*/
|
|
249
|
-
protected appendInsertValues<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[]
|
|
249
|
+
protected appendInsertValues<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[]): void;
|
|
250
250
|
/**
|
|
251
251
|
* Emit the value for a column a payload record does not provide (the column list is the union
|
|
252
252
|
* across all records). `DEFAULT` delegates to the database default; SQLite overrides this since
|
|
@@ -865,7 +865,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
865
865
|
this.appendLock(ctx, entity, q, joins);
|
|
866
866
|
}
|
|
867
867
|
insert(ctx, entity, payload, opts) {
|
|
868
|
-
this.appendInsertValues(ctx, entity, payload
|
|
868
|
+
this.appendInsertValues(ctx, entity, payload);
|
|
869
869
|
// Every engine whose ids come back from the statement itself wants the same clause, so it is
|
|
870
870
|
// appended once here instead of in an identical `insert` override per dialect.
|
|
871
871
|
if (this.insertIdSource === 'returning') {
|
|
@@ -876,7 +876,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
876
876
|
* `INSERT INTO ... VALUES (...)` and nothing more. The upsert builders extend this rather than
|
|
877
877
|
* {@link insert}: their own clause has to come before the `RETURNING`, not after it.
|
|
878
878
|
*/
|
|
879
|
-
appendInsertValues(ctx, entity, payload
|
|
879
|
+
appendInsertValues(ctx, entity, payload) {
|
|
880
880
|
const meta = getMeta(entity);
|
|
881
881
|
const payloads = fillOnFields(meta, payload, 'onInsert');
|
|
882
882
|
const keys = getInsertFieldKeys(meta, payloads);
|
package/dist/http/query.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
// the clause lists themselves, not the barrel: this module is in the browser bundle's graph
|
|
2
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } from '../type/query.js';
|
|
1
3
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
2
4
|
import { getKeys } from '../util/object.util.js';
|
|
3
|
-
const JSON_QUERY_KEYS = [
|
|
4
|
-
'$select',
|
|
5
|
-
'$populate',
|
|
6
|
-
'$exclude',
|
|
7
|
-
'$where',
|
|
8
|
-
'$sort',
|
|
9
|
-
];
|
|
10
5
|
/**
|
|
11
6
|
* Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar
|
|
12
7
|
* flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't
|
|
13
8
|
* bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties
|
|
14
9
|
* every entry to a real query/option key, so a typo or a renamed option fails to compile.
|
|
15
10
|
*/
|
|
16
|
-
const ALLOWED_QUERY_KEYS = new Set([
|
|
11
|
+
const ALLOWED_QUERY_KEYS = new Set([
|
|
12
|
+
...QUERY_OBJECT_CLAUSES,
|
|
13
|
+
...QUERY_NUMBER_CLAUSES,
|
|
14
|
+
...QUERY_BOOLEAN_CLAUSES,
|
|
15
|
+
'hardDelete',
|
|
16
|
+
'count',
|
|
17
|
+
]);
|
|
17
18
|
/**
|
|
18
19
|
* Keys that mean something locally but that this transport can never honor, so they are rejected
|
|
19
20
|
* rather than dropped like the rest. Each request runs on its own auto-committing connection, so a
|
|
@@ -35,7 +36,7 @@ export function parseQueryParams(params = {}) {
|
|
|
35
36
|
query[key] = params[key];
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
for (const key of
|
|
39
|
+
for (const key of QUERY_OBJECT_CLAUSES) {
|
|
39
40
|
const value = query[key];
|
|
40
41
|
if (typeof value === 'string') {
|
|
41
42
|
try {
|
|
@@ -47,11 +48,18 @@ export function parseQueryParams(params = {}) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
query['$where'] ??= {};
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// A query string carries every value as text, so what decodes a clause is the shape its group
|
|
52
|
+
// declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:
|
|
53
|
+
// it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.
|
|
54
|
+
for (const key of QUERY_NUMBER_CLAUSES) {
|
|
55
|
+
if (query[key] !== undefined) {
|
|
56
|
+
query[key] = Number(query[key]);
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
query[
|
|
59
|
+
for (const key of QUERY_BOOLEAN_CLAUSES) {
|
|
60
|
+
if (query[key] !== undefined) {
|
|
61
|
+
query[key] = query[key] === true || query[key] === 'true';
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
return query;
|
|
57
65
|
}
|
|
@@ -192,7 +192,7 @@ export class EntityCodeGenerator {
|
|
|
192
192
|
const lines = [];
|
|
193
193
|
// Outgoing relations (this table has FK)
|
|
194
194
|
for (const rel of table.outgoingRelations) {
|
|
195
|
-
const relCode = this.buildOutgoingRelation(rel
|
|
195
|
+
const relCode = this.buildOutgoingRelation(rel);
|
|
196
196
|
lines.push(relCode);
|
|
197
197
|
}
|
|
198
198
|
// Incoming relations (other tables have FK to this)
|
|
@@ -208,7 +208,7 @@ export class EntityCodeGenerator {
|
|
|
208
208
|
/**
|
|
209
209
|
* Build outgoing relation (ManyToOne or OneToOne where this table has FK).
|
|
210
210
|
*/
|
|
211
|
-
buildOutgoingRelation(rel
|
|
211
|
+
buildOutgoingRelation(rel) {
|
|
212
212
|
const lines = [];
|
|
213
213
|
const relatedClassName = this.options.classNameTransformer(rel.to.table.name);
|
|
214
214
|
// Try to derive property name from FK column name (e.g., author_id -> author)
|
|
@@ -63,7 +63,7 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
63
63
|
withSqlQuerier(task) {
|
|
64
64
|
return this.pool.withQuerier((querier) => {
|
|
65
65
|
if (!isSqlQuerier(querier)) {
|
|
66
|
-
throw new
|
|
66
|
+
throw new TypeError(`${this.constructor.name} requires a SQL-based querier`);
|
|
67
67
|
}
|
|
68
68
|
return task(querier);
|
|
69
69
|
});
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -154,7 +154,7 @@ export class Migrator {
|
|
|
154
154
|
if (options.to) {
|
|
155
155
|
const toIndex = selected.findIndex((m) => m.name === options.to);
|
|
156
156
|
if (toIndex === -1) {
|
|
157
|
-
throw new
|
|
157
|
+
throw new TypeError(`Migration '${options.to}' not found`);
|
|
158
158
|
}
|
|
159
159
|
selected = selected.slice(0, toIndex + 1);
|
|
160
160
|
}
|
|
@@ -277,7 +277,7 @@ export class Migrator {
|
|
|
277
277
|
async getDiffs() {
|
|
278
278
|
await this.ensureSchemaGenerator();
|
|
279
279
|
if (!this.schemaGenerator || !this.schemaIntrospector) {
|
|
280
|
-
throw new
|
|
280
|
+
throw new TypeError('Schema generator and introspector must be set');
|
|
281
281
|
}
|
|
282
282
|
const ast = await introspectSchema(this.schemaIntrospector);
|
|
283
283
|
const diffs = [];
|
|
@@ -396,7 +396,7 @@ export class Migrator {
|
|
|
396
396
|
*/
|
|
397
397
|
get generator() {
|
|
398
398
|
if (!this.schemaGenerator) {
|
|
399
|
-
throw new
|
|
399
|
+
throw new TypeError('Schema generator not set. Call setSchemaGenerator() first.');
|
|
400
400
|
}
|
|
401
401
|
return this.schemaGenerator;
|
|
402
402
|
}
|
|
@@ -439,7 +439,7 @@ export class Migrator {
|
|
|
439
439
|
}
|
|
440
440
|
async executeSqlSyncStatements(statements, options, querier) {
|
|
441
441
|
if (!isSqlQuerier(querier)) {
|
|
442
|
-
throw new
|
|
442
|
+
throw new TypeError('Migrator requires a SQL-based querier for this dialect');
|
|
443
443
|
}
|
|
444
444
|
for (const sql of statements) {
|
|
445
445
|
if (options.logging)
|
|
@@ -253,7 +253,7 @@ export class SqlSchemaGenerator {
|
|
|
253
253
|
const table = this.escapeId(tableName);
|
|
254
254
|
const colName = this.escapeId(column.name);
|
|
255
255
|
if (this.dialect.alterColumnSyntax === 'none') {
|
|
256
|
-
throw new
|
|
256
|
+
throw new TypeError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
|
|
257
257
|
`This database does not support ALTER COLUMN.`);
|
|
258
258
|
}
|
|
259
259
|
if (this.dialect.alterColumnStrategy === 'separate-clauses') {
|
|
@@ -561,7 +561,7 @@ export class SqlSchemaGenerator {
|
|
|
561
561
|
? this.escapeId(foreignKey.name)
|
|
562
562
|
: this.escapeId(`fk_${tableName}_${foreignKey.columns.join('_')}`);
|
|
563
563
|
if (!this.features.foreignKeyAlter) {
|
|
564
|
-
throw new
|
|
564
|
+
throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
565
565
|
}
|
|
566
566
|
return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
|
|
567
567
|
`FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.referencesTable)} (${refCols}) ` +
|
|
@@ -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;
|
|
@@ -3,7 +3,7 @@ import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
|
3
3
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
4
4
|
import { getMeta } from '../entity/index.js';
|
|
5
5
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
6
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, } from '../util/index.js';
|
|
6
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, someKey, } from '../util/index.js';
|
|
7
7
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
8
8
|
export const mongoDialectFeatures = {
|
|
9
9
|
explicitJsonCast: false,
|
|
@@ -77,8 +77,8 @@ export class MongoDialect extends AbstractDialect {
|
|
|
77
77
|
}
|
|
78
78
|
const meta = getMeta(entity);
|
|
79
79
|
const whereMap = buildQueryWhereAsMap(meta, where);
|
|
80
|
-
return
|
|
81
|
-
?
|
|
80
|
+
return someKey(whereMap, (key) => key === '$and' || key === '$or'
|
|
81
|
+
? whereMap[key].some((it) => this.constrainsRelations(entity, it))
|
|
82
82
|
: Boolean(meta.relations[key]));
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
@@ -153,7 +153,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
153
153
|
const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, targetCondition), opts);
|
|
154
154
|
lookups.temps.push(temp);
|
|
155
155
|
lookups.stages.push(relOpts.cardinality === 'mm' && relOpts.through
|
|
156
|
-
? this.junctionLookup(
|
|
156
|
+
? this.junctionLookup(relOpts, relMeta, relEntity, targetScope, temp, tail, opts)
|
|
157
157
|
: {
|
|
158
158
|
$lookup: {
|
|
159
159
|
from: this.resolveTableName(relEntity, relMeta),
|
|
@@ -170,7 +170,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
170
170
|
* ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
|
|
171
171
|
* lookup - the junction's filters apply too, since a soft-deleted link is not a link.
|
|
172
172
|
*/
|
|
173
|
-
junctionLookup(
|
|
173
|
+
junctionLookup(relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
|
|
174
174
|
const throughEntity = relOpts.through();
|
|
175
175
|
const throughMeta = getMeta(throughEntity);
|
|
176
176
|
const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
|
|
@@ -371,9 +371,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
371
371
|
sort(entity, sort, populate) {
|
|
372
372
|
const meta = getMeta(entity);
|
|
373
373
|
const normalized = {};
|
|
374
|
-
// The same join set the lookups are built from, so what an
|
|
375
|
-
// pipeline actually produces cannot drift apart
|
|
376
|
-
|
|
374
|
+
// The same join set the lookups are built from, so what an ordering may address and what the
|
|
375
|
+
// pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
|
|
376
|
+
// exactly as it does on the SQL dialects.
|
|
377
|
+
this.collectSort(meta, sort, resolveQueryJoins(meta, { $populate: populate, $sort: sort }), '', normalized);
|
|
377
378
|
return normalized;
|
|
378
379
|
}
|
|
379
380
|
/** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
|
|
@@ -381,6 +382,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
381
382
|
for (const [key, value] of Object.entries(sort ?? {})) {
|
|
382
383
|
const relation = meta.relations[key];
|
|
383
384
|
if (!relation) {
|
|
385
|
+
// The queried entity's own vector search is lifted out before this walk, so one reaching it
|
|
386
|
+
// sits under a relation, which a `$lookup` brings in one row at a time - there is nothing to
|
|
387
|
+
// rank. `sortDirection` would read the operator object as "ascending" and order by the raw
|
|
388
|
+
// vector column instead, which is the SQL dialects' rejection turned into a silent answer.
|
|
389
|
+
if (isVectorSearch(value)) {
|
|
390
|
+
throw new TypeError(`$vector sort is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
|
|
391
|
+
}
|
|
384
392
|
out[path + this.pathOf(meta, key)] = sortDirection(value);
|
|
385
393
|
continue;
|
|
386
394
|
}
|
|
@@ -394,8 +402,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
394
402
|
}
|
|
395
403
|
/** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
|
|
396
404
|
sortsRelations(entity, sort) {
|
|
405
|
+
if (!sort) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
397
408
|
const meta = getMeta(entity);
|
|
398
|
-
return
|
|
409
|
+
return someKey(sort, (key) => Boolean(meta.relations[key]));
|
|
399
410
|
}
|
|
400
411
|
/**
|
|
401
412
|
* Aggregate results are keyed by `$group`/`$agg` alias rather than by column, so an aggregate
|
|
@@ -445,10 +456,34 @@ export class MongoDialect extends AbstractDialect {
|
|
|
445
456
|
* for themselves and each got a different part of it wrong.
|
|
446
457
|
*/
|
|
447
458
|
readStages(entity, q, opts, extra = {}) {
|
|
448
|
-
const
|
|
449
|
-
const
|
|
459
|
+
const meta = getMeta(entity);
|
|
460
|
+
const joins = resolveQueryJoins(meta, q);
|
|
461
|
+
const lookups = this.lookupStages(meta, joins, undefined, opts);
|
|
450
462
|
const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
|
|
451
463
|
const pager = extra.pager ?? [];
|
|
464
|
+
// Merged into the query's own projection rather than standing in for one: a query that asked
|
|
465
|
+
// for no columns wants the whole document, not just the field this adds to it.
|
|
466
|
+
const projection = this.pipelineProjection(entity, q);
|
|
467
|
+
const projected = projection ? { ...projection, ...extra.project } : undefined;
|
|
468
|
+
const project = projected ? [{ $project: projected }] : [];
|
|
469
|
+
// A `$lookup` the ordering asked for puts a field on the document the caller never requested,
|
|
470
|
+
// which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
|
|
471
|
+
// it has run, so ordering by an unpopulated relation costs the same nothing it does there.
|
|
472
|
+
const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
|
|
473
|
+
const unset = sortOnly.length ? [{ $unset: sortOnly }] : [];
|
|
474
|
+
// The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
|
|
475
|
+
// that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
|
|
476
|
+
// the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
|
|
477
|
+
if (q.$distinct && sortOnly.length) {
|
|
478
|
+
throw new TypeError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
|
|
479
|
+
}
|
|
480
|
+
// `$distinct` inverts the usual order twice over: the projection decides which columns make two
|
|
481
|
+
// rows the same, so it has to run *before* the grouping, and the grouping collapses rows, so the
|
|
482
|
+
// ordering and the page have to run after it to address the set the caller actually receives.
|
|
483
|
+
const dedup = q.$distinct ? this.distinctStages(projected) : [];
|
|
484
|
+
if (dedup.length) {
|
|
485
|
+
return [...lookups, ...project, ...dedup, ...sort, ...pager];
|
|
486
|
+
}
|
|
452
487
|
// A `$required` relation drops parents when it unwinds, and an ordering may read a field only a
|
|
453
488
|
// lookup produces: either one puts the lookups first, as an INNER JOIN does. Otherwise paging
|
|
454
489
|
// first is equivalent and spares the lookups the rows it cuts.
|
|
@@ -456,11 +491,27 @@ export class MongoDialect extends AbstractDialect {
|
|
|
456
491
|
lookups.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
|
|
457
492
|
return [
|
|
458
493
|
...(lookupsFirst ? [...lookups, ...sort, ...pager] : [...sort, ...pager, ...lookups]),
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
...(projection ? [{ $project: { ...projection, ...extra.project } }] : []),
|
|
494
|
+
...unset,
|
|
495
|
+
...project,
|
|
462
496
|
];
|
|
463
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* `$distinct` as a `$group` on the columns the query projects - the same set a SQL dialect puts
|
|
500
|
+
* after `SELECT DISTINCT` - and the `$replaceRoot` that lifts the grouped key back to the top
|
|
501
|
+
* level. A query that projects nothing selects every column, primary key included, so there is
|
|
502
|
+
* nothing to collapse: `SELECT DISTINCT *` collapses nothing either.
|
|
503
|
+
*/
|
|
504
|
+
distinctStages(projection) {
|
|
505
|
+
if (!projection) {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
const keys = getKeys(projection).filter((key) => projection[key] === 1);
|
|
509
|
+
if (!keys.length) {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
const groupId = Object.fromEntries(keys.map((key) => [key, `$${key}`]));
|
|
513
|
+
return [{ $group: { _id: groupId } }, { $replaceRoot: { newRoot: '$_id' } }];
|
|
514
|
+
}
|
|
464
515
|
/**
|
|
465
516
|
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
466
517
|
* the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
|
|
@@ -487,11 +538,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
487
538
|
* aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
|
|
488
539
|
*/
|
|
489
540
|
relationStages(entity, q, opts) {
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
541
|
+
// The whole query, not `$populate` alone: an ordering by a related field needs that relation
|
|
542
|
+
// looked up just as much as selecting it does. A `$lookup` does put a field on the document
|
|
543
|
+
// where a SQL join is invisible, so the ones only the ordering asked for are unset again by
|
|
544
|
+
// {@link readStages} before the caller sees the row.
|
|
493
545
|
const meta = getMeta(entity);
|
|
494
|
-
return this.lookupStages(meta, resolveQueryJoins(meta,
|
|
546
|
+
return this.lookupStages(meta, resolveQueryJoins(meta, q), undefined, opts);
|
|
495
547
|
}
|
|
496
548
|
/**
|
|
497
549
|
* The `$lookup`/`$unwind` pair for each relation joined below `parent`, its own relations nested
|
|
@@ -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);
|
|
@@ -60,6 +61,12 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
60
61
|
parts.push(`toMany: ${toManyKeys.join(', ')}`);
|
|
61
62
|
throw new TypeError(`findManyStream does not load relations on MongoDB (${parts.join('; ')}). Use findMany with $populate (or legacy relation keys in $select) so aggregation and fill logic can run.`);
|
|
62
63
|
}
|
|
64
|
+
// An ordering that names a relation reads a field only a `$lookup` produces, and a stream has no
|
|
65
|
+
// pipeline to add one: MongoDB ranks every document equal and hands back natural order. `findMany`
|
|
66
|
+
// takes the aggregation path for exactly this case, so it is the one that can honor the clause.
|
|
67
|
+
if (this.dialect.sortsRelations(entity, q.$sort)) {
|
|
68
|
+
throw new TypeError('findManyStream does not order by a relation on MongoDB. Use findMany, whose aggregation pipeline adds the $lookup the ordering reads.');
|
|
69
|
+
}
|
|
63
70
|
const cursor = this.buildFindCursor(entity, q, opts);
|
|
64
71
|
try {
|
|
65
72
|
for await (const doc of cursor) {
|
|
@@ -143,15 +150,14 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
143
150
|
});
|
|
144
151
|
}
|
|
145
152
|
/**
|
|
146
|
-
* The `
|
|
147
|
-
*
|
|
153
|
+
* The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
|
|
154
|
+
* Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
|
|
155
|
+
* the floor, and skipped the relation-sort rejection {@link MongoDialect.sort} raises.
|
|
148
156
|
*/
|
|
149
|
-
async
|
|
157
|
+
async settleIds(entity, q, opts) {
|
|
150
158
|
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());
|
|
159
|
+
const pipeline = this.dialect.aggregationPipeline(entity, idOnlyQuery(meta, q), opts);
|
|
160
|
+
const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
155
161
|
return (this.dialect.normalizeIds(meta, founds) || []).map((found) => found[meta.id]);
|
|
156
162
|
}
|
|
157
163
|
async internalInsertMany(entity, payloads) {
|
|
@@ -176,9 +182,11 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
176
182
|
payload = clone(payload);
|
|
177
183
|
const meta = getMeta(entity);
|
|
178
184
|
const persistable = this.dialect.getPersistable(meta, payload, 'onUpdate');
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
// Settled to ids first in two cases: an `updateMany` filter cannot host a `$lookup`, so a
|
|
186
|
+
// relation condition has nowhere to go, and MongoDB takes no page on a write, so a paged one
|
|
187
|
+
// has to name the rows it picked rather than touching every match.
|
|
188
|
+
const where = this.dialect.constrainsRelations(entity, qm.$where) || isPagedQuery(qm)
|
|
189
|
+
? { _id: { $in: await this.settleIds(entity, qm, opts) } }
|
|
182
190
|
: this.dialect.where(entity, qm.$where, opts);
|
|
183
191
|
// Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
|
|
184
192
|
const update = this.dialect.getUpdateFilter(persistable);
|
|
@@ -252,8 +260,8 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
252
260
|
// Hard delete targets matching rows regardless of soft-delete state (keeps other filters).
|
|
253
261
|
const findOpts = field ? opts : { ...opts, filters: withoutSoftDeleteFilter(opts.filters) };
|
|
254
262
|
// 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.
|
|
263
|
+
// condition needs nothing extra here - and passing the whole query is what makes its page apply.
|
|
264
|
+
const ids = await this.settleIds(entity, qm, findOpts);
|
|
257
265
|
if (!ids.length) {
|
|
258
266
|
return 0;
|
|
259
267
|
}
|
|
@@ -125,7 +125,12 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
125
125
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
126
126
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
127
127
|
protected deleteRelations<E extends object>(entity: Type<E>, ids: IdValue<E>[], opts?: QueryOptions): Promise<void>;
|
|
128
|
-
|
|
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>;
|
|
129
134
|
private saveToMany;
|
|
130
135
|
private saveOneToOne;
|
|
131
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, someKey, 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
|
|
@@ -206,7 +206,7 @@ export class AbstractQuerier {
|
|
|
206
206
|
if (!id) {
|
|
207
207
|
toInsert.push(it);
|
|
208
208
|
}
|
|
209
|
-
else if (
|
|
209
|
+
else if (!someKey(it, (key) => key !== idKey)) {
|
|
210
210
|
existingIds.push(id);
|
|
211
211
|
}
|
|
212
212
|
else {
|
|
@@ -314,7 +314,7 @@ export class AbstractQuerier {
|
|
|
314
314
|
}, []);
|
|
315
315
|
if (!entries.length)
|
|
316
316
|
return;
|
|
317
|
-
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)))));
|
|
318
318
|
}
|
|
319
319
|
async updateRelations(entity, q, payload, opts) {
|
|
320
320
|
const meta = getMeta(entity);
|
|
@@ -322,9 +322,14 @@ export class AbstractQuerier {
|
|
|
322
322
|
if (!relKeys.length) {
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
|
-
const founds = await this.findMany(entity,
|
|
325
|
+
const founds = await this.findMany(entity, idOnlyQuery(meta, q), opts);
|
|
326
326
|
const ids = founds.map((found) => found[meta.id]);
|
|
327
|
-
|
|
327
|
+
if (!ids.length) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
for (const relKey of relKeys) {
|
|
331
|
+
await this.saveRelation(entity, ids, payload[relKey], relKey, true);
|
|
332
|
+
}
|
|
328
333
|
}
|
|
329
334
|
async deleteRelations(entity, ids, opts) {
|
|
330
335
|
const meta = getMeta(entity);
|
|
@@ -346,66 +351,70 @@ export class AbstractQuerier {
|
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
}
|
|
349
|
-
|
|
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) {
|
|
350
360
|
const meta = getMeta(entity);
|
|
351
|
-
const id = payload[meta.id];
|
|
352
361
|
const relOpts = meta.relations[relKey];
|
|
353
362
|
if (!relOpts)
|
|
354
363
|
return;
|
|
355
364
|
const relEntity = relOpts.entity();
|
|
356
|
-
const relPayload =
|
|
365
|
+
const relPayload = relValue;
|
|
357
366
|
switch (relOpts.cardinality) {
|
|
358
367
|
case '1m':
|
|
359
368
|
case 'mm':
|
|
360
|
-
return this.saveToMany(relOpts, relEntity,
|
|
369
|
+
return this.saveToMany(relOpts, relEntity, ids, relPayload, isUpdate);
|
|
361
370
|
case '11':
|
|
362
|
-
return this.saveOneToOne(relEntity, relOpts,
|
|
371
|
+
return this.saveOneToOne(relEntity, relOpts, ids, relPayload);
|
|
363
372
|
case 'm1':
|
|
364
373
|
if (relPayload)
|
|
365
|
-
return this.saveManyToOne(entity, relEntity, relOpts,
|
|
374
|
+
return this.saveManyToOne(entity, relEntity, relOpts, ids, relPayload);
|
|
366
375
|
}
|
|
367
376
|
}
|
|
368
|
-
async saveToMany(relOpts, relEntity,
|
|
377
|
+
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
369
378
|
const { references, through } = relOpts;
|
|
370
379
|
if (through) {
|
|
371
380
|
const localField = references[0].local;
|
|
372
381
|
const throughEntity = through();
|
|
373
382
|
if (isUpdate) {
|
|
374
|
-
await this.deleteMany(throughEntity, { $where: { [localField]:
|
|
383
|
+
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
375
384
|
}
|
|
376
385
|
if (relPayload) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
392
|
}
|
|
384
393
|
return;
|
|
385
394
|
}
|
|
386
395
|
const foreignField = references[0].foreign;
|
|
387
396
|
if (isUpdate) {
|
|
388
|
-
await this.deleteMany(relEntity, { $where: { [foreignField]:
|
|
397
|
+
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
389
398
|
}
|
|
390
399
|
if (relPayload) {
|
|
391
|
-
|
|
392
|
-
it[foreignField] = id;
|
|
393
|
-
}
|
|
394
|
-
await this.saveMany(relEntity, relPayload);
|
|
400
|
+
await this.saveMany(relEntity, ids.flatMap((id) => relPayload.map((it) => ({ ...it, [foreignField]: id }))));
|
|
395
401
|
}
|
|
396
402
|
}
|
|
397
|
-
async saveOneToOne(relEntity, relOpts,
|
|
403
|
+
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
398
404
|
const foreignField = relOpts.references[0].foreign;
|
|
399
405
|
if (relPayload === null) {
|
|
400
|
-
await this.deleteMany(relEntity, { $where: { [foreignField]:
|
|
406
|
+
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
401
407
|
return;
|
|
402
408
|
}
|
|
403
|
-
await this.
|
|
409
|
+
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
404
410
|
}
|
|
405
|
-
async saveManyToOne(entity, relEntity, relOpts,
|
|
411
|
+
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
406
412
|
const localField = relOpts.references[0].local;
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
418
|
}
|
|
410
419
|
/**
|
|
411
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);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasKeys } from '../util/object.util.js';
|
|
1
2
|
import { AbstractLocalSqliteQuerierPool } from './localSqliteQuerierPool.js';
|
|
2
3
|
/**
|
|
3
4
|
* Pool for `better-sqlite3`, or `bun:sqlite` when running under Bun - the same file, through whichever
|
|
@@ -18,7 +19,7 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
|
|
|
18
19
|
// `bun:sqlite` rejects option keys it does not know, and rejects an options object carrying no
|
|
19
20
|
// open flags, so `extensions` is stripped out and what remains of it collapses back to nothing.
|
|
20
21
|
const { extensions, ...driverOpts } = this.opts ?? {};
|
|
21
|
-
const opts =
|
|
22
|
+
const opts = hasKeys(driverOpts) ? driverOpts : undefined;
|
|
22
23
|
if (typeof Bun !== 'undefined') {
|
|
23
24
|
const { Database: BunDatabase } = await import('bun:sqlite');
|
|
24
25
|
const { adaptBunSqlite } = await import('./bunSqliteAdapter.bun.js');
|
package/dist/type/query.d.ts
CHANGED
|
@@ -215,7 +215,31 @@ export type Query<E> = {
|
|
|
215
215
|
* is what keeps the clause off those statements at the type level.
|
|
216
216
|
*/
|
|
217
217
|
$lock?: QueryLock;
|
|
218
|
-
|
|
218
|
+
/**
|
|
219
|
+
* filtering options.
|
|
220
|
+
*/
|
|
221
|
+
$where?: QueryWhere<E>;
|
|
222
|
+
/**
|
|
223
|
+
* Index from where start the search
|
|
224
|
+
*/
|
|
225
|
+
$skip?: number;
|
|
226
|
+
/**
|
|
227
|
+
* Max number of records to retrieve
|
|
228
|
+
*/
|
|
229
|
+
$limit?: number;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and
|
|
233
|
+
* a validator checking a relation's own query both need, and what each used to enumerate for itself.
|
|
234
|
+
* Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build
|
|
235
|
+
* rather than the runtime if a clause is ever renamed.
|
|
236
|
+
*
|
|
237
|
+
* `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's
|
|
238
|
+
* query accepts, so leaving it out is what excludes it from both.
|
|
239
|
+
*/
|
|
240
|
+
export declare const QUERY_OBJECT_CLAUSES: readonly ["$select", "$populate", "$exclude", "$where", "$sort"];
|
|
241
|
+
export declare const QUERY_NUMBER_CLAUSES: readonly ["$skip", "$limit"];
|
|
242
|
+
export declare const QUERY_BOOLEAN_CLAUSES: readonly ["$distinct"];
|
|
219
243
|
/**
|
|
220
244
|
* options to get a single record.
|
|
221
245
|
*/
|
package/dist/type/query.js
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and
|
|
3
|
+
* a validator checking a relation's own query both need, and what each used to enumerate for itself.
|
|
4
|
+
* Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build
|
|
5
|
+
* rather than the runtime if a clause is ever renamed.
|
|
6
|
+
*
|
|
7
|
+
* `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's
|
|
8
|
+
* query accepts, so leaving it out is what excludes it from both.
|
|
9
|
+
*/
|
|
10
|
+
export const QUERY_OBJECT_CLAUSES = [
|
|
11
|
+
'$select',
|
|
12
|
+
'$populate',
|
|
13
|
+
'$exclude',
|
|
14
|
+
'$where',
|
|
15
|
+
'$sort',
|
|
16
|
+
];
|
|
17
|
+
export const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'];
|
|
18
|
+
export const QUERY_BOOLEAN_CLAUSES = ['$distinct'];
|
|
@@ -3,10 +3,9 @@ import type { QueryPager, QuerySortDirection } from './query.js';
|
|
|
3
3
|
import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
|
|
4
4
|
/**
|
|
5
5
|
* Maps the offending keys to `never`, turning an excess key into a compile error; resolves to
|
|
6
|
-
* `unknown` (an inert intersection member) when there are none.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* checking rejects stray keys directly.
|
|
6
|
+
* `unknown` (an inert intersection member) when there are none. Needed because `$group`/`$agg` are
|
|
7
|
+
* captured as whole maps, and TypeScript skips excess-property checking on a naked type parameter.
|
|
8
|
+
* The find methods take concrete `Query<E>` params, so the native check rejects a stray key there.
|
|
10
9
|
* @internal
|
|
11
10
|
*/
|
|
12
11
|
type Reject<K> = [K] extends [never] ? unknown : Record<K & string, never>;
|
|
@@ -31,11 +31,14 @@ export type QueryWhereFieldMap<E> = {
|
|
|
31
31
|
* (untyped `Json` payloads accept any `field.suffix` path with a permissive value). Relations are
|
|
32
32
|
* filtered via nested typed objects; dotted relation paths are not supported (the dialects throw
|
|
33
33
|
* for non-JSON dotted keys).
|
|
34
|
+
*
|
|
35
|
+
* One mapped type over the three key sets rather than three intersected, for the reason
|
|
36
|
+
* {@link QuerySortMap} is: the sets are disjoint, and an assignability check against an
|
|
37
|
+
* intersection is repeated per constituent, which every `$where` in a codebase pays. The root
|
|
38
|
+
* operators stay a separate member - they are a fixed shape, not keyed off the entity.
|
|
34
39
|
*/
|
|
35
|
-
export type QueryWhereMap<E> =
|
|
36
|
-
[
|
|
37
|
-
} & {
|
|
38
|
-
[K in RelationKey<E>]?: QueryWhereMap<RelationTarget<E[K]>> | QueryRelationSizeFilter;
|
|
40
|
+
export type QueryWhereMap<E> = QueryWhereRootOperator<E> & {
|
|
41
|
+
[K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhereMap<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
|
|
39
42
|
};
|
|
40
43
|
/**
|
|
41
44
|
* Filter a to-many relation by its row count.
|
|
@@ -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,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } from '../type/query.js';
|
|
2
|
+
import { getKeys, someKey } from './object.util.js';
|
|
2
3
|
/**
|
|
3
4
|
* Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
|
|
4
5
|
* the one field it reads, so it answers for a relation being declared as well as for a resolved one.
|
|
@@ -58,17 +59,18 @@ export function getRelationRequestSummary(meta, populate) {
|
|
|
58
59
|
export function populatesRelations(meta, populate) {
|
|
59
60
|
if (!populate)
|
|
60
61
|
return false;
|
|
61
|
-
return
|
|
62
|
+
return someKey(populate, (key) => !!populate[key] && key in meta.relations);
|
|
62
63
|
}
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
64
|
+
// Taken from the clause groups declared beside `Query` itself, so a renamed clause fails to compile
|
|
65
|
+
// here instead of quietly narrowing what a relation query accepts. `$required` is the one key that
|
|
66
|
+
// is not a `Query` clause at all - it says how the relation joins, not what it selects.
|
|
67
|
+
const RELATION_QUERY_BOOLEAN_KEYS = new Set([...QUERY_BOOLEAN_CLAUSES, '$required']);
|
|
68
|
+
const RELATION_QUERY_OBJECT_KEYS = new Set(QUERY_OBJECT_CLAUSES);
|
|
69
|
+
const RELATION_QUERY_NUMBER_KEYS = new Set(QUERY_NUMBER_CLAUSES);
|
|
67
70
|
const RELATION_QUERY_ALLOWED_KEYS = new Set([
|
|
68
71
|
...RELATION_QUERY_BOOLEAN_KEYS,
|
|
69
72
|
...RELATION_QUERY_OBJECT_KEYS,
|
|
70
73
|
...RELATION_QUERY_NUMBER_KEYS,
|
|
71
|
-
'$where',
|
|
72
74
|
]);
|
|
73
75
|
function isRelationQueryObject(value) {
|
|
74
76
|
return isRecord(value) && isValidRelationQueryShape(value);
|
|
@@ -125,9 +127,6 @@ function isValidRelationQueryShape(query) {
|
|
|
125
127
|
if (RELATION_QUERY_NUMBER_KEYS.has(key) && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
126
128
|
return false;
|
|
127
129
|
}
|
|
128
|
-
if (key === '$where' && !isRecord(value)) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
130
|
}
|
|
132
131
|
return hasKnownKey;
|
|
133
132
|
}
|
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.5",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|