uql-orm 0.74.1 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- var p=[];function y(e){for(let r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,1)}}var S=["$select","$populate","$exclude","$where","$sort"],E=["$count"],T=["$skip","$limit"],f=["$candidates"],O=["$distinct"],v=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...S,...E,...T,...f,...O,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function k(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let P=o,l={message:P?.error?.message??n.statusText,code:P?.error?.code??n.status};throw y({phase:"error",error:l,opts:t}),new K(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={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:""}},V=c(s),re=new Map(V.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return k(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function F(){return q}function pe(){return F().getQuerier()}export{m as HttpQuerier,K as RequestError,R as get,pe as getQuerier,F as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,k as query,g as remove,de as setQuerierPool};
1
+ var p=[];function y(e){for(let r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,1)}}var P=["$select","$populate","$exclude","$where","$sort"],E=["$count"],T=["$skip","$limit"],f=["$candidates"],O=["$distinct"],v=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...P,...E,...T,...f,...O,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function k(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let S=o,l={message:S?.error?.message??n.statusText,code:S?.error?.code??n.status};throw y({phase:"error",error:l,opts:t}),new K(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={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:""}},F=c(s),re=new Map(F.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return k(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function V(){return q}function pe(){return V().getQuerier()}export{m as HttpQuerier,K as RequestError,R as get,pe as getQuerier,V as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,k as query,g as remove,de as setQuerierPool};
2
2
 
3
- //# debugId=5397AE2C0FC782D564756E2164756E21
3
+ //# debugId=3410F618088BC65664756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -3,7 +3,7 @@
3
3
  "sources": ["../../src/browser/http/bus.ts", "../../src/type/query.ts", "../../src/type/queryRaw.ts", "../../src/util/object.util.ts", "../../src/http/query.ts", "../../src/browser/http/http.ts", "../../src/util/string.util.ts", "../../src/http/contract.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
- "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } 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 { QueryVectorQuery, 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 * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: 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\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\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, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\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 FilterWhere<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 where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\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. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\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 * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/** The fields of `E` a vector search can rank by. */\ntype VectorFieldKey<E> = { [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never }[FieldKey<E>];\n\n/**\n * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the\n * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never\n * where the target has no vector, since an empty map would admit any value at all.\n */\nexport type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never]\n ? never\n : { [P in VectorFieldKey<E>]?: QueryVectorQuery };\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector\n * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`\n * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once\n * per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its size or nearest row.\n IsMany<E[P]> extends true\n ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>>\n : QuerySortMap<RelationTarget<E[P]>, false>\n : NonNullable<E[P]> extends readonly number[]\n ? Root extends true\n ? QuerySortValue\n : QuerySortDirection | QueryVectorQuery\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Root extends true ? QuerySortByText : unknown);\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\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 * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\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 * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\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",
6
+ "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } 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 { QueryVectorQuery, 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 * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: 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\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\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, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\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 FilterWhere<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 where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\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. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\n);\n\n/**\n * direction for the sort, and where nulls land in it.\n *\n * Unqualified, each engine has its own answer - Postgres and CockroachDB sort nulls last on `asc`, the\n * rest sort them first - so a placement is the only portable one. Engines with no `NULLS FIRST/LAST`\n * emulate it with a leading term, which no index can serve, which is why it is asked for and never\n * applied by default.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc' | QuerySortNullsDirection;\n\n/** A {@link QuerySortDirection} stating where nulls land. */\nexport type QuerySortNullsDirection = 'ascNullsFirst' | 'ascNullsLast' | 'descNullsFirst' | 'descNullsLast';\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 * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/** The fields of `E` a vector search can rank by. */\ntype VectorFieldKey<E> = { [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never }[FieldKey<E>];\n\n/**\n * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the\n * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never\n * where the target has no vector, since an empty map would admit any value at all.\n */\nexport type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never]\n ? never\n : { [P in VectorFieldKey<E>]?: QueryVectorQuery };\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector\n * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`\n * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once\n * per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its size or nearest row.\n IsMany<E[P]> extends true\n ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>>\n : QuerySortMap<RelationTarget<E[P]>, false>\n : NonNullable<E[P]> extends readonly number[]\n ? Root extends true\n ? QuerySortValue\n : QuerySortDirection | QueryVectorQuery\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Root extends true ? QuerySortByText : unknown);\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\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 * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\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 * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\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",
7
7
  "import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';\nimport type { Type } from './utility.js';\n\n/** What a `raw` callback receives. See {@link QueryRawFn}. */\nexport type QueryRawRenderOptions = {\n /** The dialect rendering the SQL. */\n dialect: SqlQueryDialect;\n /** The alias of the table in scope, unescaped; empty where there is none. */\n prefix: string;\n /** {@link prefix} escaped, with its trailing dot. */\n escapedPrefix: string;\n /** The query context the SQL is written into. */\n ctx: QueryContext;\n /**\n * The entity being rendered, which a ref read off a definition's map resolves its column against: a\n * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.\n */\n entity?: Type<unknown>;\n};\n\n/** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */\nexport type QueryRawFnOptions = Partial<QueryRawRenderOptions>;\n\n/**\n * A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else\n * it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the\n * latter rejected `({ ctx }) => ctx.append(...)`, the form every computed field is written in, because\n * TypeScript's \"returning a value where void is expected\" allowance does not apply to a union.\n */\nexport type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;\n\nexport const RAW_VALUE: unique symbol = Symbol('rawValue');\nexport const RAW_ALIAS: unique symbol = Symbol('rawAlias');\n\nexport class QueryRaw {\n readonly [RAW_VALUE]: QueryRawFn;\n readonly [RAW_ALIAS]?: string;\n\n constructor(value: QueryRawFn, alias?: string) {\n this[RAW_VALUE] = value;\n this[RAW_ALIAS] = alias;\n }\n\n /** The same expression under an alias, for a `$select` projection. */\n as(alias: string): QueryRaw {\n return new QueryRaw(this[RAW_VALUE], alias);\n }\n\n /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */\n render(opts: QueryRawRenderOptions): void {\n const emitted = this[RAW_VALUE](opts);\n if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {\n opts.ctx.append(String(emitted));\n }\n }\n}\n\n/**\n * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it\n * renders as the field's column. Its `key` is how an index tells a column from an expression.\n */\nexport class ColumnRef<K extends string = string> extends QueryRaw {\n constructor(\n readonly key: K,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n\n/**\n * A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.\n * It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and\n * sorted like any other.\n *\n * `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom\n * fields so the aggregate a field declares decides the property's type and refuses `stored: true` on\n * one no delta can maintain.\n */\nexport class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {\n declare private readonly __value: V;\n declare private readonly __storable: Storable;\n\n constructor(\n /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */\n readonly spec: RelationAggregateSpec,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n",
8
8
  "import type { EntityMeta } 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 `key` names an operator (`$eq`, `$push`...) rather than a field. */\nexport function isOperatorKey(key: string): boolean {\n return key.startsWith('$');\n}\n\n/** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether `value` is an object that is not an array, whose keys can be read. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nexport function getKeys<T extends object>(obj: T | null | undefined): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\n/** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */\nexport function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][] {\n return (Object.entries(record) as [K, V | undefined][]).filter((entry): entry is [K, V] => entry[1] !== undefined);\n}\n\n/**\n * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so\n * the fallback is what an entity that named no table is called - which is why the sites spelling this\n * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\n/**\n * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the\n * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which\n * is what a `$where` map and a composite key's id object both are; an array is a list of either.\n */\nexport function isScalarId(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) {\n return true;\n }\n if (Array.isArray(value)) {\n return false;\n }\n // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser\n // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column\n // with a map of several.\n const proto = Object.getPrototypeOf(value);\n return proto !== Object.prototype && proto !== null;\n}\n\n/** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */\nexport function isWhereMap(value: unknown): value is Record<string, unknown> {\n return !Array.isArray(value) && !isScalarId(value);\n}\n",
9
9
  "import type { QueryOptions, WireQuery } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.js';\n// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`\n// with it, in the browser bundle, which is on a size budget\nimport { RAW_VALUE } from '../type/queryRaw.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys, isWhereMap } from '../util/object.util.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof WireQuery<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 WireQuery<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> = {}): WireQuery<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n if (!isWhereMap(query['$where'])) {\n throw Object.assign(new TypeError(\"'$where' must be a JSON object\"), { status: 400 });\n }\n\n // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\n }\n\n return query as WireQuery<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 ? wireJson(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server\n * build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not\n * have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it\n * serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,\n * hits where the client's types already refuse a fragment.\n */\nexport function wireJson(value: unknown): string {\n return JSON.stringify(value, (_key: string, held: unknown) => {\n if (typeof held !== 'object' || held === null) {\n return held;\n }\n if (RAW_VALUE in held) {\n throw new TypeError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');\n }\n // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.\n if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {\n throw new TypeError('binary cannot travel over HTTP: what leaves the browser is JSON');\n }\n return held;\n });\n}\n",
@@ -13,7 +13,7 @@
13
13
  "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityWrite,\n EntityId,\n FieldKey,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdateWrite,\n WireQuery,\n WrittenId,\n} from '../../type/index.js';\nimport { isScalarId } from '../../util/object.util.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 * The URL segment an entity is addressed by, defaulting to its kebab-cased class name - the same\n * option the server handler takes, so one map serves both. State it where the default cannot: a\n * build that minifies class names renames every route.\n */\n readonly entityPath?: (entity: Type<unknown>) => string;\n};\n\n/** The id as one path segment, refusing a composite key, which has no spelling in `/:id` yet. Callers are `async`. */\nfunction idSegment<E>(entity: Type<E>, id: EntityId<E>): string {\n if (!isScalarId(id)) {\n throw new TypeError(`'${entity.name}' was addressed by an id object, which the HTTP route cannot carry.`);\n }\n return String(id);\n}\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n async findOneById<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n id: EntityId<E>,\n q?: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${basePath}/${idSegment(entity, id)}${qs}`,\n this.buildOptions(opts),\n );\n }\n\n findOne<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n return this.read<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`,\n q,\n opts,\n );\n }\n\n findMany<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: WireQuery<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<QueryFindResult<E, S, V, X, P, C>[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QueryPage<E, never>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n /** The `count` route capped at one row, so existence needs no endpoint of its own. */\n async exists<E extends object>(entity: Type<E>, q?: QueryFilter<E, never>, opts?: RequestOptions) {\n const res = await this.count(entity, { ...q, $limit: 1 }, opts);\n return { ...res, data: res.data > 0 };\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.insertMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async updateOneById<E extends object>(\n entity: Type<E>,\n id: EntityId<E>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${idSegment(entity, id)}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(\n entity: Type<E>,\n q: QuerySearch<E, never>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\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: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.saveMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${idSegment(entity, id)}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E, never>, 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}/${(this.defaults.entityPath ?? 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",
14
14
  "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"
15
15
  ],
16
- "mappings": "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",
17
- "debugId": "5397AE2C0FC782D564756E2164756E21",
16
+ "mappings": "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",
17
+ "debugId": "3410F618088BC65664756E2164756E21",
18
18
  "names": []
19
19
  }
@@ -16,10 +16,14 @@ type InsertShape<E> = {
16
16
  };
17
17
  /** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
18
18
  type HydratableField = readonly [string, HydrateKind];
19
+ /** A direction as a statement writes it: the suffix, and where the caller asked nulls to land. */
20
+ type SortOrder = {
21
+ readonly direction?: string;
22
+ readonly nulls?: 'first' | 'last';
23
+ };
19
24
  /** A sort term of a relation's rows as their aggregate orders by it: the column carrying it out. */
20
- export type SortRef = {
25
+ export type SortRef = SortOrder & {
21
26
  readonly ref: string;
22
- readonly direction: string;
23
27
  };
24
28
  /**
25
29
  * One column of a read's projection: the key its row answers under, none for a raw expression written
@@ -443,6 +447,12 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
443
447
  protected having(ctx: QueryContext, having: QueryHavingMap, emittedColumns: Record<string, string>): void;
444
448
  private static readonly SORT_DIRECTION_MAP;
445
449
  private resolveSortDirection;
450
+ /**
451
+ * One `ORDER BY` term. A placement the engine has no `NULLS FIRST/LAST` for becomes a term of its
452
+ * own in front of it, which is why one is only ever emitted where the caller asked for it: no index
453
+ * serves an expression. SQL Server needs a `CASE`, having no orderable boolean.
454
+ */
455
+ protected orderByTerm(expr: string, { direction, nulls }: SortOrder): string;
446
456
  /** Every operator of one `HAVING` condition, `AND`-joined. */
447
457
  protected havingCondition(ctx: QueryContext, expr: string, condition: QueryHavingMap[string]): void;
448
458
  /**
@@ -164,7 +164,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
164
164
  if (!order.length || (q.$limit === undefined && q.$skip === undefined)) {
165
165
  return false;
166
166
  }
167
- ctx.append(` ORDER BY ${order.map(({ ref, direction }) => ref + direction).join(', ')}`);
167
+ ctx.append(` ORDER BY ${order.map(({ ref, ...term }) => this.orderByTerm(ref, term)).join(', ')}`);
168
168
  return true;
169
169
  }
170
170
  /**
@@ -323,13 +323,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
323
323
  */
324
324
  carrySort(ctx, meta, q, opts) {
325
325
  const columns = [];
326
- const order = this.sortTerms(ctx, meta, q, opts).map(({ key, expr, direction, output }) => {
326
+ const order = this.sortTerms(ctx, meta, q, opts).map(({ key, expr, output, ...term }) => {
327
327
  if (output) {
328
- return { ref: expr, direction };
328
+ return { ref: expr, ...term };
329
329
  }
330
330
  const column = relationSortColumn(key);
331
331
  columns.push({ sql: expr, key: column });
332
- return { ref: this.escapeId(column, true), direction };
332
+ return { ref: this.escapeId(column, true), ...term };
333
333
  });
334
334
  return { columns, order };
335
335
  }
@@ -800,7 +800,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
800
800
  sort(ctx, entity, q, opts = {}) {
801
801
  const terms = this.sortTerms(ctx, getMeta(entity), q, opts);
802
802
  if (terms.length) {
803
- ctx.append(` ORDER BY ${terms.map(({ expr, direction }) => expr + direction).join(', ')}`);
803
+ ctx.append(` ORDER BY ${terms.map(({ expr, ...term }) => this.orderByTerm(expr, term)).join(', ')}`);
804
804
  }
805
805
  return terms.length > 0;
806
806
  }
@@ -837,7 +837,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
837
837
  // Where projected in the SELECT list, ordered by that alias rather than scored twice.
838
838
  const { order, project } = textSortOf(sort);
839
839
  const expr = project ? this.escapeId(project) : this.buildFragment(ctx, opts.rankText);
840
- columns.push({ key, expr, direction: this.resolveSortDirection(order), output: project !== undefined });
840
+ columns.push({ key, expr, ...this.resolveSortDirection(order), output: project !== undefined });
841
841
  continue;
842
842
  }
843
843
  if (relation) {
@@ -851,10 +851,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
851
851
  }
852
852
  const expr = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationAggregate(fragmentCtx, meta.entity, spec, prefix ?? ''));
853
853
  if (spec.search) {
854
- vectors.push({ key: name, expr, direction: '', output: false });
854
+ vectors.push({ key: name, expr, output: false });
855
855
  }
856
856
  else {
857
- columns.push({ key: keyPath, expr, direction: this.resolveSortDirection(direction), output: false });
857
+ columns.push({ key: keyPath, expr, ...this.resolveSortDirection(direction), output: false });
858
858
  }
859
859
  }
860
860
  if (rest === undefined) {
@@ -872,19 +872,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
872
872
  if (isVectorSearch(value)) {
873
873
  // Already projected in the SELECT list: order by that alias rather than recomputing it.
874
874
  vectors.push(value.$project
875
- ? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
875
+ ? { key: keyPath, expr: this.escapeId(value.$project), output: true }
876
876
  : {
877
877
  key: keyPath,
878
878
  expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value, prefix)),
879
- direction: '',
880
879
  output: false,
881
880
  });
882
881
  continue;
883
882
  }
884
- const direction = this.resolveSortDirection(value);
883
+ const order = this.resolveSortDirection(value);
885
884
  // A JSON path can sort by more than one reading, each carried under a name of its own.
886
885
  this.sortColumns(ctx, meta, key, prefix).forEach((column, index) => {
887
- columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, direction });
886
+ columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, ...order });
888
887
  });
889
888
  }
890
889
  }
@@ -1097,7 +1096,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1097
1096
  Object.entries(sort).forEach(([key, dir], index) => {
1098
1097
  if (index > 0)
1099
1098
  ctx.append(', ');
1100
- ctx.append(this.aggregateRef(emittedColumns, key, '$sort') + this.resolveSortDirection(dir));
1099
+ ctx.append(this.orderByTerm(this.aggregateRef(emittedColumns, key, '$sort'), this.resolveSortDirection(dir)));
1101
1100
  });
1102
1101
  return true;
1103
1102
  }
@@ -1119,14 +1118,36 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1119
1118
  });
1120
1119
  }
1121
1120
  static SORT_DIRECTION_MAP = new Map([
1122
- [1, ''],
1123
- ['asc', ''],
1124
- ['desc', ' DESC'],
1125
- [-1, ' DESC'],
1121
+ [1, {}],
1122
+ ['asc', {}],
1123
+ ['desc', { direction: ' DESC' }],
1124
+ [-1, { direction: ' DESC' }],
1125
+ ['ascNullsFirst', { nulls: 'first' }],
1126
+ ['ascNullsLast', { nulls: 'last' }],
1127
+ ['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
1128
+ ['descNullsLast', { direction: ' DESC', nulls: 'last' }],
1126
1129
  ]);
1127
1130
  resolveSortDirection(sort) {
1128
- const direction = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1129
- return orRefuse(direction, `unknown sort direction: ${sort}`);
1131
+ const order = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1132
+ return orRefuse(order, `unknown sort direction: ${sort}`);
1133
+ }
1134
+ /**
1135
+ * One `ORDER BY` term. A placement the engine has no `NULLS FIRST/LAST` for becomes a term of its
1136
+ * own in front of it, which is why one is only ever emitted where the caller asked for it: no index
1137
+ * serves an expression. SQL Server needs a `CASE`, having no orderable boolean.
1138
+ */
1139
+ orderByTerm(expr, { direction = '', nulls }) {
1140
+ if (!nulls) {
1141
+ return expr + direction;
1142
+ }
1143
+ const first = nulls === 'first';
1144
+ if (this.features.nullsOrdering === 'clause') {
1145
+ return `${expr}${direction} NULLS ${first ? 'FIRST' : 'LAST'}`;
1146
+ }
1147
+ const lead = this.features.nullsOrdering === 'case'
1148
+ ? `CASE WHEN ${expr} IS NULL THEN ${first ? 0 : 1} ELSE ${first ? 1 : 0} END`
1149
+ : `${expr} IS ${first ? 'NOT NULL' : 'NULL'}`;
1150
+ return `${lead}, ${expr}${direction}`;
1130
1151
  }
1131
1152
  /** Every operator of one `HAVING` condition, `AND`-joined. */
1132
1153
  havingCondition(ctx, expr, condition) {
@@ -1839,7 +1860,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1839
1860
  const key = relationTermKey(term);
1840
1861
  return [key, `${alias}.${this.escapeId(key, true)}`];
1841
1862
  }),
1842
- order: order.map(({ ref, direction }) => `${alias}.${ref}${direction}`).join(', '),
1863
+ order: order.map(({ ref, ...term }) => this.orderByTerm(`${alias}.${ref}`, term)).join(', '),
1843
1864
  };
1844
1865
  }
1845
1866
  /**
@@ -24,6 +24,11 @@ export declare const JSON_ELEM_ALIAS = "_uql_elem";
24
24
  export declare const JSON_PULL_ALIAS = "_uql_pull";
25
25
  /** The field a MongoDB `$sort` by `$text` reads `textScore` from where it projects none, taken back out after. */
26
26
  export declare const TEXT_SCORE_ALIAS = "_uql_text_score";
27
+ /**
28
+ * The field a MongoDB `$sort` placing nulls orders by first: 1 where the value is null or missing, 0
29
+ * where it is not. MongoDB takes no `NULLS FIRST`, so the placement is this flag plus the sort on it.
30
+ */
31
+ export declare function nullsSortField(path: string): string;
27
32
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
28
33
  export declare const REL_TEMP_PREFIX = "_uql_rel_";
29
34
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -25,6 +25,13 @@ export const JSON_ELEM_ALIAS = '_uql_elem';
25
25
  export const JSON_PULL_ALIAS = '_uql_pull';
26
26
  /** The field a MongoDB `$sort` by `$text` reads `textScore` from where it projects none, taken back out after. */
27
27
  export const TEXT_SCORE_ALIAS = '_uql_text_score';
28
+ /**
29
+ * The field a MongoDB `$sort` placing nulls orders by first: 1 where the value is null or missing, 0
30
+ * where it is not. MongoDB takes no `NULLS FIRST`, so the placement is this flag plus the sort on it.
31
+ */
32
+ export function nullsSortField(path) {
33
+ return `_uql_nulls_${path.replace(/\./g, '_')}`;
34
+ }
28
35
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
29
36
  export const REL_TEMP_PREFIX = '_uql_rel_';
30
37
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -31,6 +31,7 @@ export const MYSQL_FEATURES = {
31
31
  correlatedWrites: true,
32
32
  rowLocks: true,
33
33
  rowLockWithWindow: true,
34
+ nullsOrdering: 'expression',
34
35
  rowLockOf: true,
35
36
  textScoreIndexes: true,
36
37
  orderedUpsertReturning: true,
@@ -35,6 +35,7 @@ export const PG_FEATURES = {
35
35
  correlatedWrites: true,
36
36
  rowLocks: true,
37
37
  rowLockWithWindow: false,
38
+ nullsOrdering: 'clause',
38
39
  rowLockOf: true,
39
40
  textScoreIndexes: false,
40
41
  orderedUpsertReturning: true,
@@ -1,4 +1,4 @@
1
- import type { AggregateValue, ComputedRefs, EntityAggregate, EntityGetter, Except, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, RelationAggregate, TsTypeOf, Writable } from '../../type/index.js';
1
+ import type { AggregateValue, ComputedRefs, EntityAggregate, EntityGetter, Except, FieldOptions, FieldType, HasCompositeKey, IdValue, NamedIdKey, VersionKey, RejectKeys, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, RelationAggregate, TsTypeOf, Writable } from '../../type/index.js';
2
2
  import type { RejectIncompatible } from '../../util/index.js';
3
3
  /** A member decorator that also constrains the property it may be applied to, on a class `O`. */
4
4
  type MemberDecorator<V, O = unknown> = (value: undefined, context: ClassFieldDecoratorContext<O, V>) => void;
@@ -31,12 +31,14 @@ type DeclaredValue<O> = O extends {
31
31
  /**
32
32
  * The `null` a column reads back: every one holds it unless `nullable: false` says otherwise, so the
33
33
  * property admits it too. A key holds none, whether `@Id` or `@Field({ isId: true })` declares it, since
34
- * it is NOT NULL on every engine.
34
+ * it is NOT NULL on every engine, and neither does a version, which is NOT NULL DEFAULT 0.
35
35
  */
36
36
  type NullOf<O> = O extends {
37
37
  readonly nullable: false;
38
38
  } | {
39
39
  readonly isId: true;
40
+ } | {
41
+ readonly version: true;
40
42
  } ? never : null;
41
43
  /** The enum's members, or a named complaint where they widened for lack of `as const`, which would check nothing. */
42
44
  type EnumValue<Members, Declared> = Declared extends Members ? {
@@ -51,7 +53,7 @@ export declare function Field<This, O extends FieldOptions<DeclaredValue<O>, Thi
51
53
  type: FieldType;
52
54
  } | {
53
55
  references: EntityGetter;
54
- }) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O): AdmittingDecorator<DeclaredValue<O> | NullOf<O>, NullOf<O>, This>;
56
+ }) & RejectKeys<Exclude<keyof O, keyof FieldOptions>> & RejectIncompatible<O>>(opts: O & VersionIsBranded<O, This>): AdmittingDecorator<DeclaredValue<O> | NullOf<O>, NullOf<O>, This>;
55
57
  /**
56
58
  * Declares a field a relation aggregate computes, `@Field({ computed: (user) => user.resources.count() })`.
57
59
  * The aggregate says what the field holds, so it takes no `type`, and only `count` and `sum` - the two a
@@ -80,6 +82,16 @@ type AggregateOptions<E> = (Except<FieldOptions<never, E>, 'computed' | 'stored'
80
82
  type KeyIsNamed<This> = [NamedIdKey<This>] extends [never] ? {
81
83
  readonly __keyNeedsIdKeyBrand: true;
82
84
  } : unknown;
85
+ /**
86
+ * An optimistic lock the type level cannot see. `version: true` is what makes the column a lock at run
87
+ * time; the `versionKey` brand is what makes an update payload require it, and a lock only half
88
+ * declared would be a guarantee nothing enforces, so the decorator asks for both.
89
+ */
90
+ type VersionIsBranded<O, This> = O extends {
91
+ readonly version: true;
92
+ } ? [VersionKey<This>] extends [never] ? {
93
+ readonly __versionNeedsVersionKeyBrand: true;
94
+ } : unknown : unknown;
83
95
  /** {@link MemberDecorator} that also constrains the class, which is where a key is named. */
84
96
  type IdDecorator<V> = <This>(value: undefined, context: ClassFieldDecoratorContext<This, V> & KeyIsNamed<This>) => void;
85
97
  /**
@@ -213,6 +213,18 @@ export function defineEntity(entity, opts = {}) {
213
213
  meta.softDelete = softDeleteKeys[0];
214
214
  (meta.filters ??= {})[SOFT_DELETE_FILTER] = { where: { [meta.softDelete]: null }, default: true };
215
215
  }
216
+ // The optimistic lock, derived the same way and just as singular: one row has one version.
217
+ const versionKeys = getKeys(meta.fields).filter((key) => meta.fields[key]?.version);
218
+ if (versionKeys.length > 1) {
219
+ throw TypeError(`'${entity.name}' must have at most one field with 'version'`);
220
+ }
221
+ if (versionKeys.length) {
222
+ meta.version = versionKeys[0];
223
+ // Implied rather than stated: the DDL default covers a row written around the ORM, and `onInsert`
224
+ // covers MongoDB, which has no DDL to default. Both, so every backend starts a row at the same 0.
225
+ const field = meta.fields[meta.version];
226
+ meta.fields[meta.version] = { ...field, nullable: false, defaultValue: 0, onInsert: 0 };
227
+ }
216
228
  const ids = getIdKeys(meta);
217
229
  if (!ids.length) {
218
230
  throw TypeError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
@@ -2,10 +2,20 @@ import { type Document, type Filter, type Sort, type UpdateFilter } from 'mongod
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
3
  import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
4
4
  import { type CallbackKey } from '../util/index.js';
5
+ /**
6
+ * An ordering as the pipeline runs it: the `$sort`, the fields it needs on the document first - a
7
+ * placement's null flags - and the ones to take back off after it. One object, so a caller cannot
8
+ * order by a field it forgot to add, or leave one behind in the rows it answers with.
9
+ */
10
+ export type MongoSortPlan = {
11
+ readonly sort: Sort;
12
+ readonly stages: MongoAggregationPipelineEntry<Document>[];
13
+ readonly fields: string[];
14
+ };
5
15
  /** What a read pipeline contributes to {@link MongoDialect.readStages} beyond the query itself. */
6
16
  type MongoReadStages = {
7
17
  /** Ordering, which runs after the lookups when it reads one of their fields. */
8
- readonly sort?: Sort;
18
+ readonly sort?: MongoSortPlan;
9
19
  readonly pager?: MongoAggregationPipelineEntry<Document>[];
10
20
  /** A score the read answers as a field, a vector search's or a text search's; a temporary one leaves again. */
11
21
  readonly score?: {
@@ -126,7 +136,13 @@ export declare class MongoDialect extends AbstractDialect {
126
136
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
127
137
  * added for the sort alone would change what the caller gets back.
128
138
  */
129
- sort<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): Sort;
139
+ sort<E extends Document>(entity: Type<E>, q: Query<E>): Sort;
140
+ /**
141
+ * The ordering, and the paths whose nulls it places. MongoDB sorts null and missing lowest and takes
142
+ * no placement, so one is emulated: {@link nullsSortStages} flags each path and the flag is ordered
143
+ * by ahead of the value itself.
144
+ */
145
+ sortPlan<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): MongoSortPlan;
130
146
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
131
147
  private collectSort;
132
148
  /**
@@ -1,6 +1,6 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
3
+ import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
4
  import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
5
5
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { COUNT_RESULT_KEY } from '../type/query.js';
@@ -9,6 +9,7 @@ import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInte
9
9
  import { decodeBigIntsExcept } from '../util/wideNumber.js';
10
10
  import { textLanguage } from './textLanguage.js';
11
11
  import { vectorDistanceExpr } from './vectorDistance.js';
12
+ const EMPTY_SORT_PLAN = { sort: {}, stages: [], fields: [] };
12
13
  /** A scalar field's operator as the aggregation operator computing it. */
13
14
  const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
14
15
  /** Default {@link DialectFeatures} for MongoDB. */
@@ -460,10 +461,19 @@ export class MongoDialect extends AbstractDialect {
460
461
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
461
462
  * added for the sort alone would change what the caller gets back.
462
463
  */
463
- sort(entity, { $sort: sort, $populate: populate, $where: where }) {
464
+ sort(entity, q) {
465
+ return this.sortPlan(entity, q).sort;
466
+ }
467
+ /**
468
+ * The ordering, and the paths whose nulls it places. MongoDB sorts null and missing lowest and takes
469
+ * no placement, so one is emulated: {@link nullsSortStages} flags each path and the flag is ordered
470
+ * by ahead of the value itself.
471
+ */
472
+ sortPlan(entity, { $sort: sort, $populate: populate, $where: where }) {
464
473
  const meta = getMeta(entity);
465
474
  const nearest = {};
466
475
  const columns = {};
476
+ const placed = [];
467
477
  // Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
468
478
  if (sort?.$text) {
469
479
  rankedTextSearch(where);
@@ -472,12 +482,17 @@ export class MongoDialect extends AbstractDialect {
472
482
  // pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
473
483
  // exactly as it does on the SQL dialects.
474
484
  const joins = resolveQueryJoins(meta, { $populate: populate, $sort: sort });
475
- this.collectSort(meta, sort, joins, '', nearest, columns);
476
- // A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
477
- return { ...nearest, ...columns };
485
+ this.collectSort(meta, sort, joins, '', nearest, columns, placed);
486
+ const flags = Object.fromEntries(placed.map((path) => [nullsSortField(path), { $cond: [{ $eq: [{ $ifNull: [`$${path}`, null] }, null] }, 1, 0] }]));
487
+ return {
488
+ // A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
489
+ sort: { ...nearest, ...columns },
490
+ stages: placed.length ? [{ $addFields: flags }] : [],
491
+ fields: placed.map(nullsSortField),
492
+ };
478
493
  }
479
494
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
480
- collectSort(meta, sort, joins, path, nearest, out) {
495
+ collectSort(meta, sort, joins, path, nearest, out, placed) {
481
496
  for (const [key, value] of Object.entries(sort ?? {})) {
482
497
  const relation = meta.relations[key];
483
498
  if (key === '$text') {
@@ -495,7 +510,14 @@ export class MongoDialect extends AbstractDialect {
495
510
  if (isVectorSearch(value)) {
496
511
  throw new TypeError(`cannot $sort by a second vector '${key}' on MongoDB: $vectorSearch ranks by one`);
497
512
  }
498
- out[path + this.pathOf(meta, key)] = sortDirection(value);
513
+ const docPath = path + this.pathOf(meta, key);
514
+ const nulls = sortNulls(value);
515
+ if (nulls) {
516
+ placed.push(docPath);
517
+ // The flag holds 1 for a null, so ordering by it descending brings the null block to the front.
518
+ out[nullsSortField(docPath)] = nulls === 'first' ? -1 : 1;
519
+ }
520
+ out[docPath] = sortDirection(value);
499
521
  continue;
500
522
  }
501
523
  // A `$lookup` is what puts the relation's fields on the document, and only `$populate` asks for
@@ -521,7 +543,7 @@ export class MongoDialect extends AbstractDialect {
521
543
  continue;
522
544
  }
523
545
  const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, rest, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
524
- this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out);
546
+ this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out, placed);
525
547
  }
526
548
  }
527
549
  /**
@@ -699,7 +721,7 @@ export class MongoDialect extends AbstractDialect {
699
721
  return [
700
722
  ...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
701
723
  ...this.readStages(entity, q, {
702
- sort: this.sort(entity, q),
724
+ sort: this.sortPlan(entity, q),
703
725
  pager: this.pagerStages(q),
704
726
  score: text && { field: text.project ?? TEXT_SCORE_ALIAS, meta: 'textScore', temporary: !text.project },
705
727
  }),
@@ -722,10 +744,12 @@ export class MongoDialect extends AbstractDialect {
722
744
  // The value an ordering by a relation's aggregate reads, and the field it parks it on: both belong
723
745
  // with the lookups, since the `$sort` right after them is what they exist for.
724
746
  const aggregated = this.sortAggregateStages(entity, q.$sort);
747
+ const ordering = extra.sort ?? EMPTY_SORT_PLAN;
725
748
  const lookups = [...this.lookupStages(meta, joins), ...aggregated.stages];
726
749
  // Each to-many and each `$count`, which neither drop nor reorder a row, so they read the page alone.
727
750
  const related = this.relationReadStages(entity, q);
728
- const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
751
+ // The flags a placement orders by travel with their `$sort`, wherever the pipeline puts it.
752
+ const sort = hasKeys(ordering.sort) ? [...ordering.stages, { $sort: ordering.sort }] : [];
729
753
  const pager = extra.pager ?? [];
730
754
  // The score becomes a real field before anything reads it, so the lookups, the sort and the projection
731
755
  // that follow treat it like any other; merged into the query's own projection rather than standing in
@@ -743,7 +767,7 @@ export class MongoDialect extends AbstractDialect {
743
767
  // which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
744
768
  // it has run, so ordering by an unpopulated relation costs the same nothing it does there.
745
769
  const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
746
- const dropped = [...sortOnly, ...aggregated.fields];
770
+ const dropped = [...sortOnly, ...aggregated.fields, ...ordering.fields];
747
771
  const unset = dropped.length ? [{ $unset: dropped }] : [];
748
772
  // The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
749
773
  // that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
@@ -1328,9 +1352,16 @@ export class MongoDialect extends AbstractDialect {
1328
1352
  return { $vectorSearch: stage };
1329
1353
  }
1330
1354
  }
1331
- /** `-1` for the two descending spellings, `1` for everything else - MongoDB knows no other value. */
1355
+ /** `-1` for every descending spelling, `1` for everything else - MongoDB knows no other value. */
1332
1356
  function sortDirection(value) {
1333
- return value === 'desc' || value === -1 ? -1 : 1;
1357
+ return value === -1 || (typeof value === 'string' && value.startsWith('desc')) ? -1 : 1;
1358
+ }
1359
+ /** Where a direction asks nulls to land, where it asks at all. */
1360
+ function sortNulls(value) {
1361
+ if (typeof value !== 'string') {
1362
+ return undefined;
1363
+ }
1364
+ return value.endsWith('NullsFirst') ? 'first' : value.endsWith('NullsLast') ? 'last' : undefined;
1334
1365
  }
1335
1366
  /**
1336
1367
  * A `computed` field writing SQL is refused wherever a query names it, since no document engine
@@ -78,6 +78,8 @@ export class MongodbQuerier extends AbstractQuerier {
78
78
  this.dialect.constrainsRelations(entity, q.$where) ||
79
79
  this.dialect.sortsRelations(entity, q.$sort) ||
80
80
  this.dialect.readsAggregates(entity, q) ||
81
+ // A placement is a field the pipeline adds and orders by; a `find` cursor can add none.
82
+ hasKeys(this.dialect.sortPlan(entity, q).fields) ||
81
83
  textSortOf(q.$sort) !== undefined);
82
84
  }
83
85
  buildScalarProjection(entity, q) {
@@ -117,7 +119,7 @@ export class MongodbQuerier extends AbstractQuerier {
117
119
  this.dialect.buildVectorSearchStage(entity, vectorSort.vectorKey, vectorSort.vectorSearch, q.$where, q.$limit ?? 10, opts, vectorCandidates(q)),
118
120
  // `$vectorSearch` has already applied `$limit`, so the pager is its own.
119
121
  ...this.dialect.readStages(entity, q, {
120
- sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
122
+ sort: this.dialect.sortPlan(entity, { ...q, $sort: vectorSort.regularSort }),
121
123
  score: scoreAlias ? { field: scoreAlias, meta: 'vectorSearchScore' } : undefined,
122
124
  }),
123
125
  ];
@@ -33,6 +33,7 @@ const MSSQL_FEATURES = {
33
33
  correlatedWrites: true,
34
34
  rowLocks: true,
35
35
  rowLockWithWindow: true,
36
+ nullsOrdering: 'case',
36
37
  rowLockOf: true,
37
38
  textScoreIndexes: false,
38
39
  orderedUpsertReturning: false,
@@ -86,6 +86,12 @@ export declare abstract class AbstractQuerier implements Querier {
86
86
  updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
87
87
  /** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
88
88
  updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
89
+ /**
90
+ * Why an update matched no row: another writer moved the version on, or the row is gone. One read
91
+ * without the version predicate answers it, and it runs only on the failure, so the happy path
92
+ * still costs one statement. Best effort by nature - the row can change again while we ask.
93
+ */
94
+ private throwStaleVersion;
89
95
  /** The UPDATE, skipped where the payload writes no column, reporting `unwritten` instead. */
90
96
  private updateColumns;
91
97
  /**
@@ -1,6 +1,6 @@
1
1
  import { assertSoleId, getMeta, idOf, namesKey, relationOf } from '../entity/index.js';
2
2
  import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, hasKeys, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
- import { enrichError } from './queryError.js';
3
+ import { enrichError, UqlOptimisticLockError } from './queryError.js';
4
4
  /**
5
5
  * Refuses a nullish id, which would reduce to no filter at all, and a composite id missing a column,
6
6
  * which would address every row agreeing on the rest. Callers are `async`, so it always rejects.
@@ -38,6 +38,29 @@ function assertNamesRows(entity, method, q, opts) {
38
38
  }
39
39
  throw new TypeError(`'${method}' over '${entity.name}' names no rows, so it would address every one: pass '{ unfiltered: true }' to mean it`);
40
40
  }
41
+ /**
42
+ * An optimistic lock as one update applies it: the value the payload carried, out of the payload and
43
+ * into the filter, and the next one back in its place. The bump is a plain value rather than SQL
44
+ * arithmetic, since the filter pins what the column holds, which spares every engine a read-back.
45
+ */
46
+ function lockVersion(meta, key, q, row) {
47
+ const expected = row[key];
48
+ if (expected === undefined || expected === null) {
49
+ throw new TypeError(`an update of '${entityName(meta)}' carries no '${key}': a versioned row is written against the version it was read at`);
50
+ }
51
+ row[key] = (typeof expected === 'bigint' ? expected + 1n : Number(expected) + 1);
52
+ return { expected, q: { ...q, $where: { $and: [q.$where ?? {}, { [key]: expected }] } } };
53
+ }
54
+ /**
55
+ * Refuses a write that cannot carry the lock, rather than writing over whatever the row holds now.
56
+ * An upsert has no portable way to match a version - MySQL's `ON DUPLICATE KEY UPDATE` takes no
57
+ * `WHERE` - and a write the library itself composes has no version to carry.
58
+ */
59
+ function assertUnversioned(meta, method) {
60
+ if (meta.version) {
61
+ throw new TypeError(`cannot '${method}' the versioned '${entityName(meta)}': it carries no '${meta.version}' to match, so update it by id`);
62
+ }
63
+ }
41
64
  /**
42
65
  * The id each written row is named by, in payload order. Read off the rows as written, so a key the
43
66
  * database generated or the ORM filled is there, and a composite is named by every column of it.
@@ -198,9 +221,18 @@ export class AbstractQuerier {
198
221
  const meta = getMeta(entity);
199
222
  return this.hooked(entity, 'Update', [payload], async ([row]) => {
200
223
  fillOnFields(meta, [row], 'onUpdate');
224
+ const { version } = meta;
225
+ const lock = version && lockVersion(meta, version, q, row);
201
226
  const relKeys = filterPersistableRelationKeys(meta, row, 'persist');
202
227
  if (!relKeys.length && !this.settlesWrite(entity, q)) {
203
- return this.updateColumns(entity, q, row, opts, 0);
228
+ const changes = await this.updateColumns(entity, lock ? lock.q : q, row, opts, 0);
229
+ return lock && !changes ? this.throwStaleVersion(entity, version, q, lock.expected, opts) : changes;
230
+ }
231
+ if (lock) {
232
+ // Everything below reads the ids first and writes them in a second statement, which puts the
233
+ // race back in the gap between the two - the very thing the version is here to close.
234
+ // `settlesWrite` covers the paged forms, so `$sort`, `$limit` and `$skip` land here as well.
235
+ throw new TypeError(`cannot update '${entityName(meta)}' this way: a versioned row is matched and written in one statement, so it takes no '$sort', '$limit' or '$skip', writes no relation, and filters by none`);
204
236
  }
205
237
  const ids = await this.settleIds(entity, q, opts);
206
238
  if (!ids.length) {
@@ -213,6 +245,19 @@ export class AbstractQuerier {
213
245
  return changes;
214
246
  });
215
247
  }
248
+ /**
249
+ * Why an update matched no row: another writer moved the version on, or the row is gone. One read
250
+ * without the version predicate answers it, and it runs only on the failure, so the happy path
251
+ * still costs one statement. Best effort by nature - the row can change again while we ask.
252
+ */
253
+ async throwStaleVersion(entity, key, q, expected, opts) {
254
+ const meta = getMeta(entity);
255
+ const row = await this.findOne(entity, { $select: { [key]: true }, $where: q.$where }, opts);
256
+ const actual = row?.[key];
257
+ throw new UqlOptimisticLockError(actual === undefined
258
+ ? `no row of '${entityName(meta)}' matched the update: it is gone, or the filter names none`
259
+ : `'${entityName(meta)}' moved on: the payload carries '${key}' ${String(expected)}, the row is at ${String(actual)}`, expected, actual);
260
+ }
216
261
  /** The UPDATE, skipped where the payload writes no column, reporting `unwritten` instead. */
217
262
  async updateColumns(entity, q, row, opts, unwritten) {
218
263
  const writes = filterFieldKeys(getMeta(entity), row, 'onUpdate').length > 0;
@@ -241,6 +286,7 @@ export class AbstractQuerier {
241
286
  if (!meta.softDelete) {
242
287
  throw new TypeError(`'${entity.name}' has not enabled 'softDelete'`);
243
288
  }
289
+ assertUnversioned(meta, 'restoreMany');
244
290
  const $where = { ...q.$where, [meta.softDelete]: { $ne: null } };
245
291
  return this.updateMany(entity, { ...q, $where }, { [meta.softDelete]: null }, {
246
292
  filters: { softDelete: false },
@@ -249,6 +295,7 @@ export class AbstractQuerier {
249
295
  /** Fires `beforeUpsert`/`afterUpsert`: which branch a row takes is the database's to decide, so neither the insert's nor the update's pair fits. */
250
296
  async upsertOne(entity, conflictPaths, payload) {
251
297
  const meta = getMeta(entity);
298
+ assertUnversioned(meta, 'upsertOne');
252
299
  return this.hooked(entity, 'Upsert', [payload], async (rows) => {
253
300
  const { ids, changes, created } = await this.internalUpsertOne(entity, conflictPaths, rows[0]);
254
301
  adoptReportedIds(meta, rows, ids);
@@ -258,6 +305,7 @@ export class AbstractQuerier {
258
305
  }
259
306
  async upsertMany(entity, conflictPaths, payload) {
260
307
  const meta = getMeta(entity);
308
+ assertUnversioned(meta, 'upsertMany');
261
309
  return this.hooked(entity, 'Upsert', payload, async (rows) => {
262
310
  const { ids, changes } = await this.internalUpsertMany(entity, conflictPaths, rows);
263
311
  adoptReportedIds(meta, rows, ids);
@@ -295,6 +343,8 @@ export class AbstractQuerier {
295
343
  return changes;
296
344
  }
297
345
  async saveOne(entity, payload) {
346
+ // Named here as well as in `saveMany`, so the refusal names the method the caller reached for.
347
+ assertUnversioned(getMeta(entity), 'saveOne');
298
348
  const [id] = await this.saveMany(entity, [payload]);
299
349
  return id;
300
350
  }
@@ -305,6 +355,7 @@ export class AbstractQuerier {
305
355
  */
306
356
  async saveMany(entity, payload) {
307
357
  const meta = getMeta(entity);
358
+ assertUnversioned(meta, 'saveMany');
308
359
  // Indexes, not rows: the result is reported in payload order so it can be zipped with what was
309
360
  // passed, which concatenating the branches did not do.
310
361
  const toInsert = [];
@@ -409,6 +460,9 @@ export class AbstractQuerier {
409
460
  }
410
461
  /** Each parent gets its own referenced row, and its own column pointing at it. */
411
462
  async saveManyToOne(entity, relEntity, localColumn, writes) {
463
+ // Before anything is written: the follow-up that points each row at its new relation carries no
464
+ // version, and half an insert is worse than a refusal.
465
+ assertUnversioned(getMeta(entity), 'save a to-one relation of');
412
466
  const pointing = writes.filter(({ value }) => value);
413
467
  const referenceIds = await this.insertMany(relEntity, pointing.map(({ value }) => value));
414
468
  for (const [index, { id }] of pointing.entries()) {
@@ -11,7 +11,19 @@ export interface QueryError extends Error {
11
11
  * What a failed query ran into, named the same on every engine. `retryable` is a deadlock, a
12
12
  * serialization failure, a lock timeout or a busy database: the transaction can simply run again.
13
13
  */
14
- export type QueryErrorKind = 'uniqueViolation' | 'foreignKeyViolation' | 'notNullViolation' | 'checkViolation' | 'retryable';
14
+ export type QueryErrorKind = 'uniqueViolation' | 'foreignKeyViolation' | 'notNullViolation' | 'checkViolation' | 'optimisticLock' | 'retryable';
15
+ /**
16
+ * Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
17
+ * or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
18
+ * where there is no row left. `status` is what an HTTP transport answers with.
19
+ */
20
+ export declare class UqlOptimisticLockError extends Error {
21
+ readonly expected: unknown;
22
+ readonly actual: unknown;
23
+ name: string;
24
+ readonly status = 409;
25
+ constructor(message: string, expected: unknown, actual: unknown);
26
+ }
15
27
  /**
16
28
  * Names what `err` ran into on any engine, or `undefined` for anything else. Pure: the error is only
17
29
  * read, so it works on any driver error, whether or not a querier saw it first.
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
3
+ * or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
4
+ * where there is no row left. `status` is what an HTTP transport answers with.
5
+ */
6
+ export class UqlOptimisticLockError extends Error {
7
+ expected;
8
+ actual;
9
+ name = 'UqlOptimisticLockError';
10
+ status = 409;
11
+ constructor(message, expected, actual) {
12
+ super(message);
13
+ this.expected = expected;
14
+ this.actual = actual;
15
+ }
16
+ }
1
17
  /** Postgres, CockroachDB, PGlite and Neon in `code`; Bun SQL in `errno`. */
2
18
  const SQLSTATE_KINDS = new Map([
3
19
  ['23505', 'uniqueViolation'],
@@ -52,6 +68,9 @@ export function queryErrorKind(err) {
52
68
  if (typeof err !== 'object' || err === null) {
53
69
  return undefined;
54
70
  }
71
+ if (err instanceof UqlOptimisticLockError) {
72
+ return 'optimisticLock';
73
+ }
55
74
  const { code, errno, number, errorLabels, message } = err;
56
75
  const text = typeof message === 'string' ? message : '';
57
76
  return (SQLSTATE_KINDS.get(code) ??
@@ -35,6 +35,7 @@ export const SQLITE_FEATURES = {
35
35
  correlatedWrites: true,
36
36
  rowLocks: false,
37
37
  rowLockWithWindow: true,
38
+ nullsOrdering: 'clause',
38
39
  rowLockOf: true,
39
40
  textScoreIndexes: false,
40
41
  orderedUpsertReturning: true,
@@ -144,6 +144,11 @@ export interface SqlDialectFeatures extends DialectFeatures {
144
144
  readonly rowLockWithWindow: boolean;
145
145
  /** Whether a lock can be narrowed to one table of a join, `FOR UPDATE OF`, which MariaDB lacks. */
146
146
  readonly rowLockOf: boolean;
147
+ /**
148
+ * How a `$sort` states where nulls land: the `NULLS FIRST/LAST` clause, a leading `IS NULL` term
149
+ * (MySQL, MariaDB), or a leading `CASE` (SQL Server, which has no orderable boolean).
150
+ */
151
+ readonly nullsOrdering: 'clause' | 'expression' | 'case';
147
152
  /**
148
153
  * Whether a fulltext index's heavier column needs a fulltext index of its own to be scored by, as
149
154
  * MySQL's `MATCH` does, which reads only an index over exactly its columns.
@@ -6,6 +6,12 @@ import type { Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked,
6
6
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
7
7
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
8
8
  export declare const idKey: unique symbol;
9
+ /**
10
+ * Brands the property holding an entity's optimistic-lock version, which is what makes an update
11
+ * payload require it. No conventional name, unlike {@link idKey}: a field merely called `version`
12
+ * must not start demanding one.
13
+ */
14
+ export declare const versionKey: unique symbol;
9
15
  /** The filter `@Field({ softDelete })` registers, a name reserved against an entity's own filters. */
10
16
  export declare const SOFT_DELETE_FILTER = "softDelete";
11
17
  /** A filter name an entity may declare: any but {@link SOFT_DELETE_FILTER}, which a refusal names. */
@@ -30,8 +36,20 @@ export type WritableKey<E> = {
30
36
  }[FieldKey<E>];
31
37
  /** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
32
38
  export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
33
- /** A partial write as a caller supplies one: {@link UpdatePayload} without them. */
34
- export type UpdateWrite<E, Raw = QueryRaw> = UpdatePayload<E, Raw, WritableKey<E>>;
39
+ /**
40
+ * The property an entity brands with {@link versionKey} as its optimistic lock, `never` where it
41
+ * brands none. The brand is what carries `@Field({ version: true })` to the type level, since a
42
+ * decorator's options never reach `E`.
43
+ */
44
+ export type VersionKey<E> = E extends {
45
+ [versionKey]?: infer K;
46
+ } ? K & FieldKey<E> : never;
47
+ /**
48
+ * A partial write as a caller supplies one: {@link UpdatePayload} without the fields it cannot write,
49
+ * and with the version where the entity keeps one - a write that cannot say which row state it read
50
+ * is refused here rather than silently overwriting whatever is there now.
51
+ */
52
+ export type UpdateWrite<E, Raw = QueryRaw> = UpdatePayload<E, Raw, WritableKey<E>> & Required<Pick<E, VersionKey<E>>>;
35
53
  /** The relation names of an entity: every key but its fields and its methods, so the two sets cannot drift. */
36
54
  export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
37
55
  /**
@@ -247,6 +265,12 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
247
265
  * stamps `new Date()`, anything else is the value or callback stamped, `softDelete: () => Date.now()`.
248
266
  */
249
267
  readonly softDelete?: true | OnFieldCallback<V>;
268
+ /**
269
+ * Makes the column an optimistic lock: an update matches the version its payload carries and writes
270
+ * the next one, so a write against a row someone else moved on throws instead of overwriting it. The
271
+ * column is `NOT NULL DEFAULT 0`, and the entity brands the property with {@link versionKey}.
272
+ */
273
+ readonly version?: true;
250
274
  /** The SQL type, where it differs from the one `type` implies: `type: String, columnType: 'decimal'`. */
251
275
  readonly columnType?: ColumnType;
252
276
  /** A string column's length. */
@@ -643,6 +667,8 @@ export type EntityMeta<E> = {
643
667
  /** Every column of the primary key, in declaration order. */
644
668
  ids: readonly IdKey<E>[];
645
669
  softDelete?: FieldKey<E>;
670
+ /** The optimistic lock's column, from `@Field({ version: true })`. */
671
+ version?: FieldKey<E>;
646
672
  /** Named, default-on `$where` filters applied to every query unless bypassed. */
647
673
  filters?: Record<string, FilterOptions<E>>;
648
674
  fields: {
@@ -729,7 +755,10 @@ export type IndexOptions = Except<EntityIndexMeta, 'columns' | 'where'> & {
729
755
  */
730
756
  export type EntityIndexOptions<E> = Except<IndexOptions, 'include' | 'where'> & {
731
757
  readonly include?: (refs: RefMap<E>) => readonly ColumnRef<FieldKey<E>>[];
732
- /** Partial-index predicate. See {@link EntityWhere}. */
758
+ /**
759
+ * Partial-index predicate, written as the predicate the query passes: a planner matches the two by
760
+ * shape, so a hand-written `raw` that means the same thing leaves the index unused. See {@link EntityWhere}.
761
+ */
733
762
  readonly where?: EntityWhere<E>;
734
763
  };
735
764
  /**
@@ -1,5 +1,11 @@
1
1
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
2
2
  export const idKey = Symbol('idKey');
3
+ /**
4
+ * Brands the property holding an entity's optimistic-lock version, which is what makes an update
5
+ * payload require it. No conventional name, unlike {@link idKey}: a field merely called `version`
6
+ * must not start demanding one.
7
+ */
8
+ export const versionKey = Symbol('versionKey');
3
9
  /** The filter `@Field({ softDelete })` registers, a name reserved against an entity's own filters. */
4
10
  export const SOFT_DELETE_FILTER = 'softDelete';
5
11
  /** Every SQL column type a field may declare, by family: the unions below and `columnFamily` both read it. */
@@ -115,9 +115,16 @@ export type FilterOptions<E = unknown> = {
115
115
  readonly onMissing?: 'throw';
116
116
  });
117
117
  /**
118
- * direction for the sort.
119
- */
120
- export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
118
+ * direction for the sort, and where nulls land in it.
119
+ *
120
+ * Unqualified, each engine has its own answer - Postgres and CockroachDB sort nulls last on `asc`, the
121
+ * rest sort them first - so a placement is the only portable one. Engines with no `NULLS FIRST/LAST`
122
+ * emulate it with a leading term, which no index can serve, which is why it is asked for and never
123
+ * applied by default.
124
+ */
125
+ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc' | QuerySortNullsDirection;
126
+ /** A {@link QuerySortDirection} stating where nulls land. */
127
+ export type QuerySortNullsDirection = 'ascNullsFirst' | 'ascNullsLast' | 'descNullsFirst' | 'descNullsLast';
121
128
  /**
122
129
  * Accepted value for a field in `$sort` - either a direction or a vector similarity search.
123
130
  */
@@ -69,7 +69,9 @@ export function getSoftDeleteValue(field) {
69
69
  /** Fills each field `callbackKey` generates on `payload` in place, where the caller left it unset. */
70
70
  export function fillOnFields(meta, payload, callbackKey) {
71
71
  const payloads = Array.isArray(payload) ? payload : [payload];
72
- const keys = getKeys(meta.fields).filter((key) => meta.fields[key][callbackKey]);
72
+ // By presence, not truthiness, as `addInsertFieldKeys` above reads it: `onInsert: 0` and `onInsert: ''`
73
+ // are values a caller meant, and a falsy one was silently never filled.
74
+ const keys = getKeys(meta.fields).filter((key) => meta.fields[key][callbackKey] !== undefined);
73
75
  if (keys.length === 0) {
74
76
  return payloads;
75
77
  }
@@ -20,6 +20,7 @@ declare const FIELD_OPTION_FAMILY: {
20
20
  readonly onInsert: '*';
21
21
  readonly onUpdate: '*';
22
22
  readonly softDelete: '*';
23
+ readonly version: 'numeric';
23
24
  readonly columnType: '*';
24
25
  readonly length: 'string';
25
26
  readonly precision: 'numeric';
@@ -44,8 +45,15 @@ type InlineRead = (typeof INLINE_READS)[number];
44
45
  * DDL, an index, a comment, a name - so only the write half is dead on one: the engine fills it, and
45
46
  * `GENERATED ALWAYS AS` and `DEFAULT` are mutually exclusive on every engine that has both.
46
47
  */
47
- declare const GENERATED_WRITES: readonly ["updatable", "onInsert", "onUpdate", "softDelete", "defaultValue", "autoIncrement"];
48
+ declare const GENERATED_WRITES: readonly ["updatable", "onInsert", "onUpdate", "softDelete", "defaultValue", "autoIncrement", "version"];
48
49
  type GeneratedWrite = (typeof GENERATED_WRITES)[number];
50
+ /**
51
+ * What an optimistic lock cannot use: the querier writes the column on every update and matches the
52
+ * value the payload carried, so anything else deciding it would be fighting that, and the three that
53
+ * would make it another kind of column entirely. Its `nullable: false` and `DEFAULT 0` are implied.
54
+ */
55
+ declare const VERSION_WRITES: readonly ["updatable", "onInsert", "onUpdate", "softDelete", "defaultValue", "autoIncrement", "computed", "stored", "isId"];
56
+ type VersionWrite = (typeof VERSION_WRITES)[number];
49
57
  /**
50
58
  * The first option `opts` cannot use, phrased as the tail of `'Entity.field' ...`, or `undefined`
51
59
  * where every option applies. The runtime half of the decorators' check, so the imperative API and
@@ -68,7 +76,12 @@ type DeadOptions<O> = (O extends {
68
76
  readonly nullable: true;
69
77
  } ? 'nullable' : never) | (O extends {
70
78
  readonly updatable: false;
71
- } ? 'onUpdate' : never);
79
+ } ? 'onUpdate' : never) | (O extends {
80
+ readonly version: true;
81
+ } ? VersionWrite : never) | (O extends {
82
+ readonly version: true;
83
+ readonly nullable: true;
84
+ } ? 'nullable' : never);
72
85
  type Given<O> = Extract<keyof O, keyof FieldOptions>;
73
86
  type Offending<O> = {
74
87
  [K in Given<O>]: (typeof FIELD_OPTION_FAMILY)[K] extends OptionsFamily<O> | '*' ? K extends DeadOptions<O> ? K : never : K;
@@ -21,6 +21,7 @@ const FIELD_OPTION_FAMILY = {
21
21
  onInsert: '*',
22
22
  onUpdate: '*',
23
23
  softDelete: '*',
24
+ version: 'numeric',
24
25
  columnType: '*',
25
26
  length: 'string',
26
27
  precision: 'numeric',
@@ -46,12 +47,8 @@ const INLINE_READS = [
46
47
  'eager',
47
48
  'distance',
48
49
  ];
49
- /**
50
- * What a column the *database* writes cannot use. A stored computed column is a real column - it has
51
- * DDL, an index, a comment, a name - so only the write half is dead on one: the engine fills it, and
52
- * `GENERATED ALWAYS AS` and `DEFAULT` are mutually exclusive on every engine that has both.
53
- */
54
- const GENERATED_WRITES = [
50
+ /** Every option that decides what a column holds, or whether it is written at all. */
51
+ const VALUE_DECIDERS = [
55
52
  'updatable',
56
53
  'onInsert',
57
54
  'onUpdate',
@@ -60,19 +57,37 @@ const GENERATED_WRITES = [
60
57
  'autoIncrement',
61
58
  ];
62
59
  /**
63
- * Whatever leaves `key` unread, named for the message, or `undefined` where the field reads it. Only
64
- * `nullable: true` contradicts a key: `nullable: false` says what the key already is, and rejecting
65
- * an accurate statement teaches an author to distrust the check.
60
+ * What a column the *database* writes cannot use. A stored computed column is a real column - it has
61
+ * DDL, an index, a comment, a name - so only the write half is dead on one: the engine fills it, and
62
+ * `GENERATED ALWAYS AS` and `DEFAULT` are mutually exclusive on every engine that has both.
63
+ */
64
+ const GENERATED_WRITES = [...VALUE_DECIDERS, 'version'];
65
+ /**
66
+ * What an optimistic lock cannot use: the querier writes the column on every update and matches the
67
+ * value the payload carried, so anything else deciding it would be fighting that, and the three that
68
+ * would make it another kind of column entirely. Its `nullable: false` and `DEFAULT 0` are implied.
66
69
  */
70
+ const VERSION_WRITES = [...VALUE_DECIDERS, 'computed', 'stored', 'isId'];
71
+ /**
72
+ * Whether `key` is the `nullable: true` a NOT NULL column contradicts. `nullable: false` says what
73
+ * such a column already is, and rejecting an accurate statement teaches an author to distrust the check.
74
+ */
75
+ function contradictsNotNull(opts, key) {
76
+ return key === 'nullable' && opts.nullable === true;
77
+ }
78
+ /** Whatever leaves `key` unread, named for the message, or `undefined` where the field reads it. */
67
79
  function deadOn(opts, key) {
68
80
  if (isInlinedExpression(opts) && !INLINE_READS.some((read) => read === key))
69
81
  return 'an inlined computed field';
70
82
  if (opts.stored === true && GENERATED_WRITES.some((write) => write === key))
71
83
  return 'a stored computed column';
72
- if (opts.isId === true && key === 'nullable' && opts.nullable === true)
84
+ if (opts.isId === true && contradictsNotNull(opts, key))
73
85
  return 'a primary key';
74
86
  if (opts.updatable === false && key === 'onUpdate')
75
87
  return "a field declared 'updatable: false'";
88
+ if (opts.version === true && (VERSION_WRITES.some((write) => write === key) || contradictsNotNull(opts, key))) {
89
+ return 'a version field';
90
+ }
76
91
  return undefined;
77
92
  }
78
93
  /**
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.74.1",
6
+ "version": "0.76.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -74,12 +74,17 @@ export class Post {
74
74
  }
75
75
  ```
76
76
 
77
- - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as
78
- `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
79
- - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match:
80
- `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
77
+ - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
78
+ - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match: `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
81
79
  - Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
82
80
  - `@ManyToMany({ entity: () => Tag, through: () => PostTag })` names its junction entity.
81
+ - `@Index((post) => [post.authorId], { where: { archived: { $ne: true } } })` states a partial index's filter as the predicate the query passes, never as `raw`: a planner matches the two by shape, so `raw` that means the same thing leaves the index unused.
82
+ - `@Field({ type: Number, version: true })`, with `[versionKey]?: 'version'` on the class, makes the column an
83
+ optimistic lock: every update payload must carry the version it read (a compile error otherwise), the update
84
+ matches on it and writes the next one, and a write against a row someone else moved on throws
85
+ `UqlOptimisticLockError` (`status` 409) instead of overwriting it. Save, upsert and `restoreMany` are refused on such
86
+ an entity (restore with `updateOneById`, `{ filters: { softDelete: false } }`); `updateMany` writes only the rows
87
+ still at the version it carries, and delete needs none.
83
88
  - `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
84
89
 
85
90
  ## Queries
@@ -100,6 +105,10 @@ const users = await pool.findMany(User, {
100
105
 
101
106
  - The keys are `$select`, `$exclude`, `$where`, `$populate`, `$count`, `$distinct`, `$sort`, `$skip`, `$limit`;
102
107
  `$count: { posts: true }` tallies a to-many under `_count` without loading it.
108
+ - `$sort` takes `'asc'`/`1` or `'desc'`/`-1`, and `'ascNullsLast'`, `'ascNullsFirst'`, `'descNullsFirst'` or
109
+ `'descNullsLast'` to say where nulls land, which reads the same on every engine (emulated where there is no
110
+ `NULLS FIRST`). Unqualified, each engine keeps its own answer: Postgres and CockroachDB sort nulls last on `asc`,
111
+ the rest sort them first.
103
112
  - `$where` takes a value for equality or an operator map: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`,
104
113
  `$nin`, `$between`, `$like`, `$ilike`, `$regex`, `$startsWith`, `$endsWith`, `$includes`, `$isNull`,
105
114
  `$isNotNull`. `$and`, `$or`, `$not` and `$nor` combine clauses.