uql-orm 0.31.4 → 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 CHANGED
@@ -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 roughly 3x over the next closest and an order of magnitude over the slowest, on Bun, Node and Deno alike.
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 ntetaker and video summarizer for Zoom, Meet, Slack, and Teams. Instant summaries with action items in 35+ languages. Built by UQL's author.
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 W=[];function F(B){for(let V of W)V(B)}function _(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function N(B,V){return G(B,{method:"get"},V)}function Q(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},k=z($),R=new Map(k.filter((B)=>$[B].method==="GET"&&$[B].path!=="/:id").map((B)=>[$[B].path,B]));function D(B){return K(B.name)}var c=["$select","$populate","$exclude","$where","$sort"],v=new Set([...c,"$skip","$limit","hardDelete","count"]);function A(B){if(!B)return"";let V=new URLSearchParams;for(let X of z(B)){let Z=B[X];if(Z===void 0)continue;V.append(X,typeof Z==="object"&&Z!==null?JSON.stringify(Z):String(Z))}let I=V.toString();return I?`?${I}`:""}class C{basePath;defaults;constructor(B,V={}){this.basePath=B;this.defaults=V}findOneById(B,V,I,X){let Z=this.getBasePath(B),H=A(I);return N(`${Z}/${V}${H}`,this.buildOptions(X))}findOne(B,V,I){return this.read(`${this.getBasePath(B)}${$.findOne.path}`,V,I)}findMany(B,V,I){let X={...V};if(I?.count)X.count=!0;return this.read(this.getBasePath(B),X,I)}async findManyAndCount(B,V,I){let X=await this.findMany(B,V,{...I,count:!0});if(typeof X.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...X,count:X.count}}count(B,V,I){return this.read(`${this.getBasePath(B)}${$.count.path}`,V,I)}insertOne(B,V,I){let X=this.getBasePath(B);return Q(X,V,this.buildOptions(I))}insertMany(B,V,I){let X=this.getBasePath(B);return Q(`${X}${$.insertMany.path}`,V,this.buildOptions(I))}updateOneById(B,V,I,X){let Z=this.getBasePath(B);return J(`${Z}/${V}`,I,this.buildOptions(X))}updateMany(B,V,I,X){let Z=this.getBasePath(B),H=A(V);return J(`${Z}${H}`,I,this.buildOptions(X))}saveOne(B,V,I){let X=this.getBasePath(B);return L(X,V,this.buildOptions(I))}saveMany(B,V,I){let X=this.getBasePath(B);return L(`${X}${$.saveMany.path}`,V,this.buildOptions(I))}deleteOneById(B,V,I={}){let X=this.getBasePath(B),Z=I.hardDelete?A({hardDelete:I.hardDelete}):"";return x(`${X}/${V}${Z}`,this.buildOptions(I))}deleteMany(B,V,I={}){let X=this.getBasePath(B),Z=A(I.hardDelete?{...V,hardDelete:I.hardDelete}:V);return x(`${X}${Z}`,this.buildOptions(I))}getBasePath(B){return`${this.basePath}/${D(B)}`}read(B,V,I){if(this.defaults.readMethod==="QUERY")return T(B,V??{},this.buildOptions(I));return N(`${B}${A(V)}`,this.buildOptions(I))}buildOptions(B){if(!this.defaults.headers&&!B?.headers)return B;return{...B,headers:{...this.defaults.headers,...B?.headers}}}}var P={getQuerier:()=>new C("/api")};function y(B){P=B}function g(){return P}function p(){return g().getQuerier()}export{y as setQuerierPool,x as remove,T as query,L as put,Q as post,J as patch,_ as on,F as notify,g as getQuerierPool,p as getQuerier,N as get,f as RequestError,C as HttpQuerier};
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=BB4E96BEA0CE335F64756E2164756E21
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
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": "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",
15
- "debugId": "BB4E96BEA0CE335F64756E2164756E21",
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[], opts?: QueryOptions): void;
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, opts);
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, opts) {
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);
@@ -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([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count']);
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 JSON_QUERY_KEYS) {
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
- if (query['$skip']) {
51
- query['$skip'] = Number(query['$skip']);
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
- if (query['$limit']) {
54
- query['$limit'] = Number(query['$limit']);
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, table);
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, table) {
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 Error(`${this.constructor.name} requires a SQL-based querier`);
66
+ throw new TypeError(`${this.constructor.name} requires a SQL-based querier`);
67
67
  }
68
68
  return task(querier);
69
69
  });
@@ -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 Error(`Migration '${options.to}' not found`);
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 Error('Schema generator and introspector must be set');
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 Error('Schema generator not set. Call setSchemaGenerator() first.');
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 Error('Migrator requires a SQL-based querier for this dialect');
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 Error(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
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 Error(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
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}) ` +
@@ -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 Object.entries(whereMap).some(([key, val]) => key === '$and' || key === '$or'
81
- ? val.some((it) => this.constrainsRelations(entity, it))
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(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts)
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(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
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 `ORDER BY` may address and what the
375
- // pipeline actually produces cannot drift apart.
376
- this.collectSort(meta, sort, resolveQueryJoins(meta, { $populate: populate }), '', normalized);
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 Object.keys(sort ?? {}).some((key) => Boolean(meta.relations[key]));
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,17 +456,27 @@ 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 lookups = this.relationStages(entity, q, opts);
449
- const projection = this.pipelineProjection(entity, q);
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 ?? [];
452
- // A `$required` relation drops parents when it unwinds, and an ordering may read a field only a
453
- // lookup produces: either one puts the lookups first, as an INNER JOIN does. Otherwise paging
454
- // first is equivalent and spares the lookups the rows it cuts.
455
464
  // Merged into the query's own projection rather than standing in for one: a query that asked
456
465
  // for no columns wants the whole document, not just the field this adds to it.
466
+ const projection = this.pipelineProjection(entity, q);
457
467
  const projected = projection ? { ...projection, ...extra.project } : undefined;
458
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
+ }
459
480
  // `$distinct` inverts the usual order twice over: the projection decides which columns make two
460
481
  // rows the same, so it has to run *before* the grouping, and the grouping collapses rows, so the
461
482
  // ordering and the page have to run after it to address the set the caller actually receives.
@@ -468,7 +489,11 @@ export class MongoDialect extends AbstractDialect {
468
489
  // first is equivalent and spares the lookups the rows it cuts.
469
490
  const lookupsFirst = this.sortsRelations(entity, q.$sort) ||
470
491
  lookups.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
471
- return [...(lookupsFirst ? [...lookups, ...sort, ...pager] : [...sort, ...pager, ...lookups]), ...project];
492
+ return [
493
+ ...(lookupsFirst ? [...lookups, ...sort, ...pager] : [...sort, ...pager, ...lookups]),
494
+ ...unset,
495
+ ...project,
496
+ ];
472
497
  }
473
498
  /**
474
499
  * `$distinct` as a `$group` on the columns the query projects - the same set a SQL dialect puts
@@ -513,11 +538,12 @@ export class MongoDialect extends AbstractDialect {
513
538
  * aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
514
539
  */
515
540
  relationStages(entity, q, opts) {
516
- // Resolved from `$populate` alone, deliberately: on the SQL dialects a `$sort` can add a join of
517
- // its own because a join is invisible in the result, while a `$lookup` puts a field on the
518
- // document. Same join model, and this backend takes the part of it that it can carry.
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.
519
545
  const meta = getMeta(entity);
520
- return this.lookupStages(meta, resolveQueryJoins(meta, { $populate: q.$populate }), undefined, opts);
546
+ return this.lookupStages(meta, resolveQueryJoins(meta, q), undefined, opts);
521
547
  }
522
548
  /**
523
549
  * The `$lookup`/`$unwind` pair for each relation joined below `parent`, its own relations nested
@@ -61,6 +61,12 @@ export class MongodbQuerier extends AbstractQuerier {
61
61
  parts.push(`toMany: ${toManyKeys.join(', ')}`);
62
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.`);
63
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
+ }
64
70
  const cursor = this.buildFindCursor(entity, q, opts);
65
71
  try {
66
72
  for await (const doc of cursor) {
@@ -1,5 +1,5 @@
1
1
  import { getMeta } from '../entity/index.js';
2
- import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, 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 (Object.keys(it).length === 1) {
209
+ else if (!someKey(it, (key) => key !== idKey)) {
210
210
  existingIds.push(id);
211
211
  }
212
212
  else {
@@ -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 = Object.keys(driverOpts).length > 0 ? driverOpts : undefined;
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');
@@ -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
- } & QueryFilter<E> & QueryPager;
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
  */
@@ -1 +1,18 @@
1
- export {};
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. Used by `aggregate`'s `$group`,
7
- * which is captured as a generic (a bare generic skips excess-property checking). The find methods
8
- * don't need this: they take concrete `Query<E>` params, so TypeScript's native excess-property
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> = QueryWhereFieldMap<E> & QueryWhereRootOperator<E> & {
36
- [P in JsonFieldPaths<E>]?: QueryWhereFieldValue<JsonFieldPathValue<E, P>>;
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,3 +1,4 @@
1
+ import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } from '../type/query.js';
1
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
@@ -60,15 +61,16 @@ export function populatesRelations(meta, populate) {
60
61
  return false;
61
62
  return someKey(populate, (key) => !!populate[key] && key in meta.relations);
62
63
  }
63
- // Keep in sync with `Query`'s own keys (`type/query.ts`); `$where`'s value type is `QueryWhere` (`type/queryWhere.ts`).
64
- const RELATION_QUERY_BOOLEAN_KEYS = new Set(['$distinct', '$required']);
65
- const RELATION_QUERY_OBJECT_KEYS = new Set(['$select', '$populate', '$exclude', '$sort']);
66
- const RELATION_QUERY_NUMBER_KEYS = new Set(['$limit', '$skip']);
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.4",
6
+ "version": "0.31.5",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"