uql-orm 0.26.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,7 @@ from the browser to the server. The same object runs on every supported database
58
58
  - **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [virtual fields](https://uql-orm.dev/entities/virtual-fields) are sub-queries you can filter on, and a migration can be plain SQL.
59
59
  - **Light.** Zero runtime dependencies, 305 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
60
60
  - **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
61
- - **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark): 278µs, against 621µs for the next closest and 1,889µs for the slowest.
61
+ - **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark): 239µs, against 812µs for the next closest and 2,286µs for the slowest.
62
62
 
63
63
  ## Get started
64
64
 
@@ -1,4 +1,4 @@
1
- var W=[];function F(B){for(let V of W)V(B)}function g(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function Q(B,V){return G(B,{method:"get"},V)}function N(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},P=z($),b=new Map(P.filter((B)=>$[B].method==="GET"&&$[B].path!=="/:id").map((B)=>[$[B].path,B]));function k(B){return K(B.name)}var c=["$select","$populate","$exclude","$where","$sort"],v=new Set([...c,"$skip","$limit","hardDelete","count"]);function A(B){if(!B)return"";let V=new URLSearchParams;for(let X of z(B)){let Z=B[X];if(Z===void 0)continue;V.append(X,typeof Z==="object"&&Z!==null?JSON.stringify(Z):String(Z))}let I=V.toString();return I?`?${I}`:""}class C{basePath;defaults;constructor(B,V={}){this.basePath=B;this.defaults=V}findOneById(B,V,I,X){let Z=this.getBasePath(B),H=A(I);return Q(`${Z}/${V}${H}`,this.buildOptions(X))}findOne(B,V,I){return this.read(`${this.getBasePath(B)}${$.findOne.path}`,V,I)}findMany(B,V,I){let X={...V};if(I?.count)X.count=!0;return this.read(this.getBasePath(B),X,I)}async findManyAndCount(B,V,I){let X=await this.findMany(B,V,{...I,count:!0});if(typeof X.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...X,count:X.count}}count(B,V,I){return this.read(`${this.getBasePath(B)}${$.count.path}`,V,I)}insertOne(B,V,I){let X=this.getBasePath(B);return N(X,V,this.buildOptions(I))}insertMany(B,V,I){let X=this.getBasePath(B);return N(`${X}${$.insertMany.path}`,V,this.buildOptions(I))}updateOneById(B,V,I,X){let Z=this.getBasePath(B);return J(`${Z}/${V}`,I,this.buildOptions(X))}updateMany(B,V,I,X){let Z=this.getBasePath(B),H=A(V);return J(`${Z}${H}`,I,this.buildOptions(X))}saveOne(B,V,I){let X=this.getBasePath(B);return L(X,V,this.buildOptions(I))}saveMany(B,V,I){let X=this.getBasePath(B);return L(`${X}${$.saveMany.path}`,V,this.buildOptions(I))}deleteOneById(B,V,I={}){let X=this.getBasePath(B),Z=I.hardDelete?A({hardDelete:I.hardDelete}):"";return x(`${X}/${V}${Z}`,this.buildOptions(I))}deleteMany(B,V,I={}){let X=this.getBasePath(B),Z=A(I.hardDelete?{...V,hardDelete:I.hardDelete}:V);return x(`${X}${Z}`,this.buildOptions(I))}getBasePath(B){return`${this.basePath}/${k(B)}`}read(B,V,I){if(this.defaults.readMethod==="QUERY")return T(B,V??{},this.buildOptions(I));return Q(`${B}${A(V)}`,this.buildOptions(I))}buildOptions(B){if(!this.defaults.headers&&!B?.headers)return B;return{...B,headers:{...this.defaults.headers,...B?.headers}}}}var D={getQuerier:()=>new C("/api")};function y(B){D=B}function _(){return D}function p(){return _().getQuerier()}export{y as setQuerierPool,x as remove,T as query,L as put,N as post,J as patch,g as on,F as notify,_ as getQuerierPool,p as getQuerier,Q as get,f as RequestError,C as HttpQuerier};
1
+ var W=[];function F(B){for(let V of W)V(B)}function _(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function N(B,V){return G(B,{method:"get"},V)}function Q(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},k=z($),b=new Map(k.filter((B)=>$[B].method==="GET"&&$[B].path!=="/:id").map((B)=>[$[B].path,B]));function D(B){return K(B.name)}var c=["$select","$populate","$exclude","$where","$sort"],v=new Set([...c,"$skip","$limit","hardDelete","count"]);function A(B){if(!B)return"";let V=new URLSearchParams;for(let X of z(B)){let Z=B[X];if(Z===void 0)continue;V.append(X,typeof Z==="object"&&Z!==null?JSON.stringify(Z):String(Z))}let I=V.toString();return I?`?${I}`:""}class C{basePath;defaults;constructor(B,V={}){this.basePath=B;this.defaults=V}findOneById(B,V,I,X){let Z=this.getBasePath(B),H=A(I);return N(`${Z}/${V}${H}`,this.buildOptions(X))}findOne(B,V,I){return this.read(`${this.getBasePath(B)}${$.findOne.path}`,V,I)}findMany(B,V,I){let X={...V};if(I?.count)X.count=!0;return this.read(this.getBasePath(B),X,I)}async findManyAndCount(B,V,I){let X=await this.findMany(B,V,{...I,count:!0});if(typeof X.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...X,count:X.count}}count(B,V,I){return this.read(`${this.getBasePath(B)}${$.count.path}`,V,I)}insertOne(B,V,I){let X=this.getBasePath(B);return Q(X,V,this.buildOptions(I))}insertMany(B,V,I){let X=this.getBasePath(B);return Q(`${X}${$.insertMany.path}`,V,this.buildOptions(I))}updateOneById(B,V,I,X){let Z=this.getBasePath(B);return J(`${Z}/${V}`,I,this.buildOptions(X))}updateMany(B,V,I,X){let Z=this.getBasePath(B),H=A(V);return J(`${Z}${H}`,I,this.buildOptions(X))}saveOne(B,V,I){let X=this.getBasePath(B);return L(X,V,this.buildOptions(I))}saveMany(B,V,I){let X=this.getBasePath(B);return L(`${X}${$.saveMany.path}`,V,this.buildOptions(I))}deleteOneById(B,V,I={}){let X=this.getBasePath(B),Z=I.hardDelete?A({hardDelete:I.hardDelete}):"";return x(`${X}/${V}${Z}`,this.buildOptions(I))}deleteMany(B,V,I={}){let X=this.getBasePath(B),Z=A(I.hardDelete?{...V,hardDelete:I.hardDelete}:V);return x(`${X}${Z}`,this.buildOptions(I))}getBasePath(B){return`${this.basePath}/${D(B)}`}read(B,V,I){if(this.defaults.readMethod==="QUERY")return T(B,V??{},this.buildOptions(I));return N(`${B}${A(V)}`,this.buildOptions(I))}buildOptions(B){if(!this.defaults.headers&&!B?.headers)return B;return{...B,headers:{...this.defaults.headers,...B?.headers}}}}var P={getQuerier:()=>new C("/api")};function y(B){P=B}function g(){return P}function p(){return g().getQuerier()}export{y as setQuerierPool,x as remove,T as query,L as put,Q as post,J as patch,_ as on,F as notify,g as getQuerierPool,p as getQuerier,N as get,f as RequestError,C as HttpQuerier};
2
2
 
3
- //# debugId=89C8E712B5A364FC64756E2164756E21
3
+ //# debugId=63F5785EAD0E521C64756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -7,11 +7,11 @@
7
7
  "import type { FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/** Whether `obj` has at least two enumerable keys. */\nexport function hasMultipleKeys(obj: object): boolean {\n let count = 0;\n for (const _ in obj) {\n if (++count > 1) return true;\n }\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(obj: object, pred: (key: string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */\nexport function someValue(obj: object, pred: (value: unknown) => boolean): boolean {\n return someKey(obj, (key) => pred((obj as Record<string, unknown>)[key]));\n}\n\nconst isOperatorKey = (key: string) => key.startsWith('$');\n\n/**\n * Whether `value` is a non-empty object whose keys are query/update operators (`$eq`, `$push`, ...).\n * The single source of this test: the SQL dialects, the MongoDB dialect and the `$elemMatch` walker\n * all classify operator objects with it, and they used to disagree about `{}`.\n */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */\nexport function isOperatorOnlyObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));\n}\n\nexport function getKeys<T extends object>(obj: T): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\nexport function getFieldKeys<E>(\n fields: {\n [K in FieldKey<E>]?: FieldOptions;\n },\n): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\n}\n",
8
8
  "export function kebabCase(val: string): string {\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n resp += val[i] === val[i].toUpperCase() ? '-' + val[i].toLowerCase() : val[i];\n }\n return resp;\n}\n\nexport function upperFirst(text: string): string {\n if (!text) return text;\n return text[0].toUpperCase() + text.slice(1);\n}\n\nexport function lowerFirst(text: string): string {\n if (!text) return text;\n return text[0].toLowerCase() + text.slice(1);\n}\n\nexport function snakeCase(val: string): string {\n if (val === null || val === undefined) return val as string;\n if (!val) return '';\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n const char = val[i];\n const charLower = char.toLowerCase();\n if (char !== charLower && char === char.toUpperCase()) {\n resp += '_' + charLower;\n } else {\n resp += char;\n }\n }\n return resp;\n}\n\n/**\n * Convert a string to PascalCase (UpperCamelCase).\n * @example 'user_profile' -> 'UserProfile'\n * @example 'some-text' -> 'SomeText'\n */\nexport function pascalCase(str: string): string {\n if (!str) return '';\n return str\n .split(/[_\\s-]+/)\n .map((word) => {\n // Lower-casing the rest is only right for a word that carries no case of its own: it turns\n // `USER_ID` into `UserId`, but it also turns `tenantId` into `Tenantid`.\n const rest = word === word.toUpperCase() ? word.slice(1).toLowerCase() : word.slice(1);\n return word.charAt(0).toUpperCase() + rest;\n })\n .join('');\n}\n\n/**\n * Convert a string to camelCase.\n * @example 'user_profile' -> 'userProfile'\n * @example 'SomeText' -> 'someText'\n */\nexport function camelCase(str: string): string {\n const pascal = pascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/**\n * Simple singularize function for English words.\n * @example 'users' -> 'user'\n * @example 'categories' -> 'category'\n */\nexport function singularize(name: string): string {\n if (!name) return '';\n if (name.endsWith('ies')) {\n return name.slice(0, -3) + 'y';\n }\n if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) {\n return name.slice(0, -2);\n }\n if (name.endsWith('s') && !name.endsWith('ss')) {\n return name.slice(0, -1);\n }\n return name;\n}\n\n/**\n * Simple pluralize function for English words.\n * @example 'user' -> 'users'\n * @example 'category' -> 'categories'\n */\nexport function pluralize(name: string): string {\n if (!name) return '';\n if (name.endsWith('y') && name.length > 1 && !/[aeiou]/.test(name[name.length - 2])) {\n return name.slice(0, -1) + 'ies';\n }\n if (name.endsWith('s') || name.endsWith('x') || name.endsWith('z') || name.endsWith('ch') || name.endsWith('sh')) {\n return name + 'es';\n }\n return name + 's';\n}\n",
9
9
  "import type { Type, UniversalQuerier } from '../type/index.js';\n// the specific util modules, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\nimport { kebabCase } from '../util/string.util.js';\n\ntype RouteShape = {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n readonly path: '' | `/${string}`;\n};\n\n/**\n * Single source of truth for the CRUD-over-HTTP surface, shared by server adapters and the browser client.\n * Keys are constrained to {@link UniversalQuerier} method names, so renaming a querier method\n * (or routing a non-existent one) is a compile error.\n */\nexport const CRUD_ROUTES = {\n findMany: { method: 'GET', path: '' },\n findOne: { method: 'GET', path: '/one' },\n count: { method: 'GET', path: '/count' },\n findOneById: { method: 'GET', path: '/:id' },\n insertOne: { method: 'POST', path: '' },\n insertMany: { method: 'POST', path: '/many' },\n saveOne: { method: 'PUT', path: '' },\n saveMany: { method: 'PUT', path: '/many' },\n updateMany: { method: 'PATCH', path: '' },\n updateOneById: { method: 'PATCH', path: '/:id' },\n deleteOneById: { method: 'DELETE', path: '/:id' },\n deleteMany: { method: 'DELETE', path: '' },\n} as const satisfies Partial<Record<keyof UniversalQuerier, RouteShape>>;\n\nexport type CrudOperation = keyof typeof CRUD_ROUTES;\n\nexport type CrudRoute = (typeof CRUD_ROUTES)[CrudOperation];\n\n/**\n * `QUERY` (RFC 10008) is an alternate transport for the read operations: same semantics as the\n * GET routes, but the JSON query travels in the request body instead of the query string,\n * avoiding URL-length limits for large queries.\n */\nexport type HttpMethod = CrudRoute['method'] | 'QUERY';\n\nconst CRUD_OPS = getKeys(CRUD_ROUTES);\n\n// derived from CRUD_ROUTES (the literal-path GET routes) so the sub-paths live in exactly one place\nconst QUERY_READ_OPS: ReadonlyMap<string, CrudOperation> = new Map(\n CRUD_OPS.filter((op) => CRUD_ROUTES[op].method === 'GET' && CRUD_ROUTES[op].path !== '/:id').map((op) => [\n CRUD_ROUTES[op].path,\n op,\n ]),\n);\n\n/**\n * URL segment for an entity, e.g. `entityPath(UserProfile) === 'user-profile'`.\n */\nexport function entityPath<E>(entity: Type<E>): string {\n return kebabCase(entity.name);\n}\n\nexport type RouteMatch = {\n readonly op: CrudOperation;\n /**\n * the resolved transport method - differs from the op's canonical route method for QUERY.\n */\n readonly method: HttpMethod;\n readonly id?: string;\n};\n\n/**\n * Resolve a (method, sub-path) pair to a CRUD operation. Literal sub-paths win over `:id`.\n */\nexport function matchRoute(method: string, subPath: string | undefined): RouteMatch | undefined {\n const raw = method.toUpperCase();\n const literal = subPath === undefined ? '' : `/${subPath}`;\n if (raw === 'QUERY') {\n const op = QUERY_READ_OPS.get(literal);\n return op ? { op, method: 'QUERY' } : undefined;\n }\n // HEAD reads like GET per HTTP semantics; the server runtime omits the response body\n const verb = raw === 'HEAD' ? 'GET' : raw;\n let idOp: CrudOperation | undefined;\n for (const op of CRUD_OPS) {\n const route = CRUD_ROUTES[op];\n if (route.method !== verb) {\n continue;\n }\n if (route.path === literal) {\n return { op, method: route.method };\n }\n if (route.path === '/:id') {\n idOp = op;\n }\n }\n return idOp && subPath !== undefined ? { op: idOp, method: CRUD_ROUTES[idOp].method, id: subPath } : undefined;\n}\n\nexport type RequestSuccessResponse<E> = {\n data: E;\n count?: number;\n};\n\nexport type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {\n count: number;\n};\n\nexport type RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
10
- "import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
10
+ "import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof Query<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
11
11
  "import {\n CRUD_ROUTES,\n entityPath,\n type HttpMethod,\n type RequestCountedSuccessResponse,\n type RequestSuccessResponse,\n} from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type { IdValue, Query, QueryOne, QueryOptions, QuerySearch, Type, UpdatePayload } from '../../type/index.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n};\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n findOneById<E extends object>(\n entity: Type<E>,\n id: IdValue<E>,\n q?: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<E | undefined>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n findOne<E extends object>(\n entity: Type<E>,\n q: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n return this.read<E | undefined>(`${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`, q, opts);\n }\n\n findMany<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<E[]>> {\n const data: Query<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<E[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<E[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.insertMany.path}`, payload, this.buildOptions(opts));\n }\n\n updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${id}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.saveMany.path}`, payload, this.buildOptions(opts));\n }\n\n deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${entityPath(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
12
12
  "import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
13
13
  ],
14
- "mappings": "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",
15
- "debugId": "89C8E712B5A364FC64756E2164756E21",
14
+ "mappings": "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",
15
+ "debugId": "63F5785EAD0E521C64756E2164756E21",
16
16
  "names": []
17
17
  }
@@ -22,6 +22,11 @@ export declare class CockroachDialect extends PgLikeSqlDialect {
22
22
  * not supported" (both verified on v26.2), so neither is offered here.
23
23
  */
24
24
  protected readonly indexFeatures: Set<IndexFeature>;
25
+ /**
26
+ * `noKeyUpdate`/`keyShare` are omitted on purpose, not by oversight: CockroachDB parses both and
27
+ * treats them as aliases of `FOR UPDATE`/`FOR SHARE`, so offering them would hand back a stronger
28
+ * lock than was asked for, with nothing signalling it.
29
+ */
25
30
  /**
26
31
  * CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
27
32
  * vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
@@ -27,6 +27,11 @@ export class CockroachDialect extends PgLikeSqlDialect {
27
27
  * not supported" (both verified on v26.2), so neither is offered here.
28
28
  */
29
29
  indexFeatures = new Set(['expression', 'partial', 'include']);
30
+ /**
31
+ * `noKeyUpdate`/`keyShare` are omitted on purpose, not by oversight: CockroachDB parses both and
32
+ * treats them as aliases of `FOR UPDATE`/`FOR SHARE`, so offering them would hand back a stronger
33
+ * lock than was asked for, with nothing signalling it.
34
+ */
30
35
  /**
31
36
  * CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
32
37
  * vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
@@ -184,6 +184,20 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
184
184
  getComparisonKey<E>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, { prefix }?: QueryOptions): void;
185
185
  sort<E>(ctx: QueryContext, entity: Type<E>, sort: QuerySortMap<E> | undefined, { prefix }: QueryOptions): void;
186
186
  pager(ctx: QueryContext, opts: QueryPager): void;
187
+ /** Whether this engine has row locks at all. The SQLite family locks the database instead. */
188
+ readonly supportsRowLocks: boolean;
189
+ /** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
190
+ readonly supportsLockOf: boolean;
191
+ /** Whether this statement joins, which is what forces the lock to be narrowed to one table. */
192
+ private joinsRelations;
193
+ /** Validated before the querier checks for a transaction, so the clearer error wins. */
194
+ assertLockSupported<E>(entity: Type<E>, q: Query<E>): void;
195
+ /**
196
+ * The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
197
+ * joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
198
+ * and the other engines quietly widen the lock to the joined rows.
199
+ */
200
+ protected appendLock<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>): void;
187
201
  count<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
188
202
  /** `$group` aggregate operator → SQL function name. An allowlist, not a formatter: the op key
189
203
  * comes from query data, so anything outside this map must be rejected rather than passed
@@ -1,5 +1,5 @@
1
1
  import { getMeta } from '../entity/index.js';
2
- import { QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
2
+ import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
3
3
  import { asSelectMap, buildQueryWhereAsMap, buildSortMap, escapeSqlId, fillOnFields, filterFieldKeys, flatObject, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, hasMultipleKeys, isBooleanType, isJsonType, isJsonUpdateOp, isNumericType, isOperatorObject, isOperatorOnlyObject, isPopulatingRelations, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationAtKey, parseRelationSize, raw, someValue, withoutSoftDeleteFilter, } from '../util/index.js';
4
4
  import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
5
5
  import { IndexSqlDialect } from './indexSqlDialect.js';
@@ -746,6 +746,42 @@ export class AbstractSqlDialect extends IndexSqlDialect {
746
746
  ctx.append(` OFFSET ${Number(opts.$skip)}`);
747
747
  }
748
748
  }
749
+ /** Whether this engine has row locks at all. The SQLite family locks the database instead. */
750
+ supportsRowLocks = true;
751
+ /** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
752
+ supportsLockOf = true;
753
+ /** Whether this statement joins, which is what forces the lock to be narrowed to one table. */
754
+ joinsRelations(meta, q) {
755
+ return getRelationRequestSummary(meta, q.$populate).joinableKeys.length > 0;
756
+ }
757
+ /** Validated before the querier checks for a transaction, so the clearer error wins. */
758
+ assertLockSupported(entity, q) {
759
+ if (!parseQueryLock(q.$lock)) {
760
+ return;
761
+ }
762
+ if (!this.supportsRowLocks) {
763
+ throw new TypeError(`${this.dialectName} does not support row-level locking ($lock)`);
764
+ }
765
+ if (!this.supportsLockOf && this.joinsRelations(getMeta(entity), q)) {
766
+ throw new TypeError(`${this.dialectName} cannot narrow a row lock to one table, so $lock cannot be combined with a joined $populate`);
767
+ }
768
+ }
769
+ /**
770
+ * The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
771
+ * joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
772
+ * and the other engines quietly widen the lock to the joined rows.
773
+ */
774
+ appendLock(ctx, entity, q) {
775
+ const wait = parseQueryLock(q.$lock);
776
+ if (!wait) {
777
+ return;
778
+ }
779
+ this.assertLockSupported(entity, q);
780
+ const meta = getMeta(entity);
781
+ const target = this.joinsRelations(meta, q) ? ` OF ${this.escapeId(this.resolveTableName(entity, meta))}` : '';
782
+ const suffix = wait === 'skip' ? ' SKIP LOCKED' : wait === 'nowait' ? ' NOWAIT' : '';
783
+ ctx.append(` FOR UPDATE${target}${suffix}`);
784
+ }
749
785
  count(ctx, entity, q, opts) {
750
786
  const search = { ...q };
751
787
  delete search.$sort;
@@ -860,6 +896,9 @@ export class AbstractSqlDialect extends IndexSqlDialect {
860
896
  find(ctx, entity, q = {}, opts) {
861
897
  this.select(ctx, entity, q.$select, q.$exclude, q.$populate, opts, q.$distinct, q.$sort);
862
898
  this.search(ctx, entity, q, opts);
899
+ // Appended here rather than in `search`, which `count`/`update`/`delete` share: a lock belongs
900
+ // to a SELECT alone. Every engine spells it after LIMIT/OFFSET, so it goes last.
901
+ this.appendLock(ctx, entity, q);
863
902
  }
864
903
  insert(ctx, entity, payload, opts) {
865
904
  this.appendInsertValues(ctx, entity, payload, opts);
@@ -55,6 +55,10 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
55
55
  */
56
56
  protected indexKeyword(index: IndexSchema): string;
57
57
  protected readonly indexFeatures: Set<IndexFeature>;
58
+ /**
59
+ * No `FOR NO KEY UPDATE`/`FOR KEY SHARE`: those are PostgreSQL's weaker pair and the family has
60
+ * no equivalent, so asking for one is rejected rather than served a stronger lock.
61
+ */
58
62
  protected indexAccessMethod(index: IndexSchema): string;
59
63
  protected numericCast(expr: string): string;
60
64
  protected ilikeExpr(f: string, ph: string): string;
@@ -102,6 +102,10 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
102
102
  return index.type === 'fulltext' ? 'FULLTEXT INDEX' : super.indexKeyword(index);
103
103
  }
104
104
  indexFeatures = new Set(['expression', 'prefixLength']);
105
+ /**
106
+ * No `FOR NO KEY UPDATE`/`FOR KEY SHARE`: those are PostgreSQL's weaker pair and the family has
107
+ * no equivalent, so asking for one is rejected rather than served a stronger lock.
108
+ */
105
109
  indexAccessMethod(index) {
106
110
  return index.type && index.type !== 'fulltext' ? ` USING ${index.type}` : '';
107
111
  }
@@ -14,6 +14,13 @@ const JSON_QUERY_KEYS = [
14
14
  * every entry to a real query/option key, so a typo or a renamed option fails to compile.
15
15
  */
16
16
  const ALLOWED_QUERY_KEYS = new Set([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count']);
17
+ /**
18
+ * Keys that mean something locally but that this transport can never honor, so they are rejected
19
+ * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a
20
+ * row lock taken here is released before the response is written: honoring `$lock` is impossible,
21
+ * and ignoring it would hand the caller a read they believe is serialized and is not.
22
+ */
23
+ const REJECTED_QUERY_KEYS = new Set(['$lock']);
17
24
  /**
18
25
  * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
19
26
  * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
@@ -21,6 +28,9 @@ const ALLOWED_QUERY_KEYS = new Set([...JSON_QUERY_KEYS, '$skip', '$limit', 'hard
21
28
  export function parseQueryParams(params = {}) {
22
29
  const query = {};
23
30
  for (const key of getKeys(params)) {
31
+ if (REJECTED_QUERY_KEYS.has(key)) {
32
+ throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });
33
+ }
24
34
  if (ALLOWED_QUERY_KEYS.has(key)) {
25
35
  query[key] = params[key];
26
36
  }
@@ -9,6 +9,8 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
9
9
  * family shares and drops expressions.
10
10
  */
11
11
  protected readonly indexFeatures: Set<IndexFeature>;
12
+ /** MariaDB has no `FOR ... OF`, so a lock cannot be narrowed to one table of a join. */
13
+ readonly supportsLockOf = false;
12
14
  /** Unlike MySQL: `VECTOR(n)` takes its dimension, and its vector index is declared inline. */
13
15
  protected readonly featureOverrides: Partial<DialectFeatures>;
14
16
  /** MariaDB 10.5+ supports `INSERT ... RETURNING`, so the ids are exact per row. */
@@ -11,6 +11,8 @@ export class MariaDialect extends MysqlLikeSqlDialect {
11
11
  * family shares and drops expressions.
12
12
  */
13
13
  indexFeatures = new Set(['prefixLength']);
14
+ /** MariaDB has no `FOR ... OF`, so a lock cannot be narrowed to one table of a join. */
15
+ supportsLockOf = false;
14
16
  /** Unlike MySQL: `VECTOR(n)` takes its dimension, and its vector index is declared inline. */
15
17
  featureOverrides = {
16
18
  vectorSupportsLength: true,
@@ -64,6 +64,12 @@ export declare class MongoDialect extends AbstractDialect {
64
64
  private compareRelationCount;
65
65
  /** Whether a query subtracts `key` from the projection, via `$exclude` or a negative `$select`. */
66
66
  private subtractsKey;
67
+ /**
68
+ * MongoDB has no row-level lock to map `$lock` onto: its concurrency control is the transaction
69
+ * plus atomic document updates. Rejected rather than ignored, like `raw()` below, since a dropped
70
+ * lock silently removes the mutual exclusion the caller asked for.
71
+ */
72
+ assertNoLock<E>(q: Query<E>): void;
67
73
  /** `raw()` renders SQL, so it has no MongoDB equivalent - say so instead of emitting `{}`. */
68
74
  private assertNoRaw;
69
75
  /**
@@ -221,6 +221,16 @@ export class MongoDialect extends AbstractDialect {
221
221
  const at = (map) => map?.[key];
222
222
  return at(exclude) === true || at(select) === false;
223
223
  }
224
+ /**
225
+ * MongoDB has no row-level lock to map `$lock` onto: its concurrency control is the transaction
226
+ * plus atomic document updates. Rejected rather than ignored, like `raw()` below, since a dropped
227
+ * lock silently removes the mutual exclusion the caller asked for.
228
+ */
229
+ assertNoLock(q) {
230
+ if (q.$lock !== undefined) {
231
+ throw new TypeError('$lock (row-level locking) is not supported on MongoDB');
232
+ }
233
+ }
224
234
  /** `raw()` renders SQL, so it has no MongoDB equivalent - say so instead of emitting `{}`. */
225
235
  assertNoRaw(value) {
226
236
  if (value instanceof QueryRaw) {
@@ -18,6 +18,7 @@ export class MongodbQuerier extends AbstractQuerier {
18
18
  });
19
19
  }
20
20
  async internalFindMany(entity, q, opts) {
21
+ this.dialect.assertNoLock(q);
21
22
  return this.timed('internalFindMany', undefined, async () => {
22
23
  const meta = getMeta(entity);
23
24
  const vectorSort = this.dialect.extractVectorSort(q.$sort);
@@ -33,6 +33,17 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
33
33
  protected lazyConnect(): Promise<void>;
34
34
  all<T>(query: string, values?: unknown[]): Promise<T[]>;
35
35
  run(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
36
+ /**
37
+ * `$lock` outside a transaction is always a bug, and a silent one. Every engine accepts
38
+ * `SELECT ... FOR UPDATE` in autocommit and then releases the lock as the statement commits,
39
+ * before the caller has seen a row: the SQL is correct, nothing is omitted, and no layer below
40
+ * this one can tell. The dialect cannot check it either, being stateless and shared by every
41
+ * connection of the pool, so this is the only place it can be caught.
42
+ *
43
+ * The capability check runs first on purpose: "this engine has no row locks" is the more
44
+ * actionable answer, and on SQLite it is the answer either way.
45
+ */
46
+ protected assertLockable<E>(entity: Type<E>, q: Query<E>): void;
36
47
  protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
37
48
  protected internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncGenerator<Awaited<E>, void, unknown>;
38
49
  /**
@@ -51,7 +51,27 @@ export class AbstractSqlQuerier extends AbstractQuerier {
51
51
  return this.timed(query, values, () => this.internalRun(query, this.dialect.normalizeValues(values)));
52
52
  });
53
53
  }
54
+ /**
55
+ * `$lock` outside a transaction is always a bug, and a silent one. Every engine accepts
56
+ * `SELECT ... FOR UPDATE` in autocommit and then releases the lock as the statement commits,
57
+ * before the caller has seen a row: the SQL is correct, nothing is omitted, and no layer below
58
+ * this one can tell. The dialect cannot check it either, being stateless and shared by every
59
+ * connection of the pool, so this is the only place it can be caught.
60
+ *
61
+ * The capability check runs first on purpose: "this engine has no row locks" is the more
62
+ * actionable answer, and on SQLite it is the answer either way.
63
+ */
64
+ assertLockable(entity, q) {
65
+ if (!q.$lock) {
66
+ return;
67
+ }
68
+ this.dialect.assertLockSupported(entity, q);
69
+ if (!this.hasOpenTransaction) {
70
+ throw new TypeError('$lock requires an open transaction');
71
+ }
72
+ }
54
73
  async internalFindMany(entity, q, opts) {
74
+ this.assertLockable(entity, q);
55
75
  const ctx = this.dialect.createContext();
56
76
  this.dialect.find(ctx, entity, q, opts);
57
77
  const res = await this.all(ctx.sql, ctx.values);
@@ -60,6 +80,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
60
80
  return founds;
61
81
  }
62
82
  async *internalFindManyStream(entity, q, opts) {
83
+ this.assertLockable(entity, q);
63
84
  const meta = getMeta(entity);
64
85
  const { toManyKeys } = getRelationRequestSummary(meta, q.$populate);
65
86
  if (toManyKeys.length) {
@@ -12,6 +12,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
12
12
  readonly rollbackTransactionCommand = "ROLLBACK";
13
13
  readonly isolationLevelStrategy = "none";
14
14
  readonly alterColumnSyntax = "none";
15
+ /** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
16
+ readonly supportsRowLocks = false;
15
17
  readonly booleanLiteral = "integer";
16
18
  readonly insertIdSource = "returning";
17
19
  /**
@@ -26,6 +26,8 @@ export class SqliteDialect extends AbstractSqlDialect {
26
26
  rollbackTransactionCommand = 'ROLLBACK';
27
27
  isolationLevelStrategy = 'none';
28
28
  alterColumnSyntax = 'none';
29
+ /** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
30
+ supportsRowLocks = false;
29
31
  booleanLiteral = 'integer';
30
32
  // SQLite supports `RETURNING` (including on `INSERT ... ON CONFLICT`), so IDs are exact per row.
31
33
  insertIdSource = 'returning';
@@ -1,7 +1,7 @@
1
1
  import type { ForeignKeyAction, IndexType } from '../schema/types.js';
2
2
  import type { FilterOptions } from './query.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
- import type { DistributiveOmit, Json, Scalar, Type, Unpacked } from './utility.js';
4
+ import type { Except, Json, Scalar, Type, Unpacked } from './utility.js';
5
5
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
6
6
  /**
7
7
  * Allow to customize the name of the property that identifies an entity
@@ -579,10 +579,10 @@ export type EntityOptions<E = unknown> = {
579
579
  };
580
580
  /**
581
581
  * Everything an index carries beyond its columns, shared by `@Index`, `defineEntity` and the
582
- * migration builder's `table.index(...)`. `DistributiveOmit` (not plain `Omit`) keeps `type`/
583
- * `distance` a discriminated pair: omitting `distance` on a vector index type is a compile error.
582
+ * migration builder's `table.index(...)`. `Except` (not plain `Omit`) keeps `type`/`distance` a
583
+ * discriminated pair: omitting `distance` on a vector index type is a compile error.
584
584
  */
585
- export type IndexOptions = DistributiveOmit<EntityIndexMeta, 'columns'>;
585
+ export type IndexOptions = Except<EntityIndexMeta, 'columns'>;
586
586
  /**
587
587
  * An index as authored, before `defineIndex` normalizes its columns.
588
588
  */
@@ -9,6 +9,7 @@ export * from './querier.js';
9
9
  export * from './querierPool.js';
10
10
  export * from './query.js';
11
11
  export * from './queryAggregate.js';
12
+ export * from './queryLock.js';
12
13
  export * from './queryRaw.js';
13
14
  export * from './queryWhere.js';
14
15
  export * from './universalQuerier.js';
@@ -9,6 +9,7 @@ export * from './querier.js';
9
9
  export * from './querierPool.js';
10
10
  export * from './query.js';
11
11
  export * from './queryAggregate.js';
12
+ export * from './queryLock.js';
12
13
  export * from './queryRaw.js';
13
14
  export * from './queryWhere.js';
14
15
  export * from './universalQuerier.js';
@@ -1,7 +1,8 @@
1
1
  import type { FieldKey, JsonFieldPaths, RelationKey } from './entity.js';
2
+ import type { QueryLock } from './queryLock.js';
2
3
  import type { QueryRaw } from './queryRaw.js';
3
4
  import type { QueryWhere } from './queryWhere.js';
4
- import type { BooleanLike, PrimaryKey, Unpacked } from './utility.js';
5
+ import type { BooleanLike, Except, PrimaryKey, Unpacked } from './utility.js';
5
6
  import type { QueryVectorSearch } from './vector.js';
6
7
  export type QueryOptions = {
7
8
  /**
@@ -65,7 +66,7 @@ export type QueryConflictPaths<E> = {
65
66
  /**
66
67
  * options to populate a relation.
67
68
  */
68
- export type QueryPopulateRelationOptions<E> = (E extends unknown[] ? Query<Unpacked<E>> : QueryUnique<Unpacked<E>>) & {
69
+ export type QueryPopulateRelationOptions<E> = (E extends unknown[] ? Except<Query<Unpacked<E>>, '$lock'> : QueryUnique<Unpacked<E>>) & {
69
70
  $required?: boolean;
70
71
  };
71
72
  /**
@@ -180,11 +181,21 @@ export type Query<E> = {
180
181
  * whether to return only distinct rows.
181
182
  */
182
183
  $distinct?: boolean;
184
+ /**
185
+ * take a row-level lock on the rows this query returns (`SELECT ... FOR UPDATE`). Needs an open
186
+ * transaction: outside one the statement commits and drops the lock before the caller can act on
187
+ * the rows, so it is rejected rather than emitted. Locks only the queried entity, never anything
188
+ * reached through `$populate`. SQL only; MongoDB and the SQLite family reject it.
189
+ *
190
+ * Deliberately declared here rather than on `QuerySearch`, which `count`/`update`/`delete` take:
191
+ * that placement is what keeps the clause off those statements at the type level.
192
+ */
193
+ $lock?: QueryLock;
183
194
  } & QuerySearch<E>;
184
195
  /**
185
196
  * options to get a single record.
186
197
  */
187
- export type QueryOne<E> = Omit<Query<E>, '$limit'>;
198
+ export type QueryOne<E> = Except<Query<E>, '$limit'>;
188
199
  /**
189
200
  * options to get an unique record.
190
201
  */
@@ -0,0 +1,21 @@
1
+ declare const QUERY_LOCK_WAITS: readonly ['block', 'nowait', 'skip'];
2
+ /**
3
+ * What to do about a row someone else already holds. `block` (the default) waits for them; `nowait`
4
+ * fails the statement at once; `skip` leaves the row out of the result, which is what makes a
5
+ * work-queue possible: each worker takes rows nobody else has.
6
+ */
7
+ export type QueryLockWait = (typeof QUERY_LOCK_WAITS)[number];
8
+ /**
9
+ * `true` takes the lock and waits for anyone holding the rows; the object form chooses what to do
10
+ * instead of waiting. `false` takes none, so a query built conditionally needs no branch.
11
+ */
12
+ export type QueryLock = boolean | {
13
+ readonly wait?: QueryLockWait;
14
+ };
15
+ /**
16
+ * The wait policy this lock resolves to, or `undefined` when there is no lock. An unknown policy
17
+ * throws here rather than reaching a dialect, so the message names what the caller wrote instead of
18
+ * the SQL it would have produced.
19
+ */
20
+ export declare function parseQueryLock(lock: QueryLock | undefined): QueryLockWait | undefined;
21
+ export {};
@@ -0,0 +1,19 @@
1
+ const QUERY_LOCK_WAITS = ['block', 'nowait', 'skip'];
2
+ function isOneOf(vals, val) {
3
+ return vals.includes(val);
4
+ }
5
+ /**
6
+ * The wait policy this lock resolves to, or `undefined` when there is no lock. An unknown policy
7
+ * throws here rather than reaching a dialect, so the message names what the caller wrote instead of
8
+ * the SQL it would have produced.
9
+ */
10
+ export function parseQueryLock(lock) {
11
+ if (lock === undefined || lock === false) {
12
+ return undefined;
13
+ }
14
+ const wait = lock === true ? 'block' : (lock?.wait ?? 'block');
15
+ if (!isOneOf(QUERY_LOCK_WAITS, wait)) {
16
+ throw new TypeError(`unknown $lock wait policy: ${String(wait)}`);
17
+ }
18
+ return wait;
19
+ }
@@ -47,10 +47,14 @@ export type Writable<T> = {
47
47
  -readonly [K in keyof T]: T[K];
48
48
  };
49
49
  /**
50
- * `Omit`, but distributed over `T`'s union members before recombining. Plain `Omit<T, K>` computes
51
- * `keyof T` up front, which for a union takes the intersection of each member's keys and flattens
52
- * their property types together - collapsing a discriminated union (e.g. `EntityIndexMeta`'s
53
- * `type`/`distance` pairing) into a single, non-discriminated shape.
50
+ * `Omit`, fixed on three counts. Its key has to exist, where `Omit<T, K extends keyof any>` lets a
51
+ * typo or a renamed property silently omit nothing. Being a homomorphic mapped type it distributes
52
+ * over unions, where `Omit` intersects each member's keys and flattens a discriminated union (e.g.
53
+ * `EntityIndexMeta`'s `type`/`distance` pairing) into one non-discriminated shape. And it removes
54
+ * the key from types carrying an index signature, where `Exclude<keyof T, K>` widens back to
55
+ * `string | number` and leaves the key in place.
54
56
  */
55
- export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
57
+ export type Except<T, K extends keyof T> = {
58
+ [P in keyof T as P extends K ? never : P]: T[P];
59
+ };
56
60
  export type Unpacked<T> = T extends readonly (infer U)[] ? U : T extends (...args: unknown[]) => infer U ? U : T extends Promise<infer U> ? U : T;
@@ -1,4 +1,4 @@
1
- import type { EntityMeta, Query, QueryPopulate, RelationKey } from '../type/index.js';
1
+ import type { EntityMeta, Except, Query, QueryPopulate, RelationKey } from '../type/index.js';
2
2
  export type RelationRequestSummary<E> = {
3
3
  readonly requestedKeys: RelationKey<E>[];
4
4
  readonly joinableKeys: RelationKey<E>[];
@@ -7,7 +7,7 @@ export type RelationRequestSummary<E> = {
7
7
  export declare function getRelationRequestSummary<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): RelationRequestSummary<E>;
8
8
  /** True when `$populate` includes at least one relation key. */
9
9
  export declare function isPopulatingRelations<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): boolean;
10
- export type RelationQuery<E extends object = object> = Query<E> & {
10
+ export type RelationQuery<E extends object = object> = Except<Query<E>, '$lock'> & {
11
11
  $required?: boolean;
12
12
  };
13
13
  export type ParsedRelationQuery<E extends object = object> = {
@@ -41,6 +41,11 @@ function isRelationQueryObject(value) {
41
41
  return isRecord(value) && isValidRelationQueryShape(value);
42
42
  }
43
43
  export function parseRelationQueryValue(value) {
44
+ // Caught before the shape check so the message names the key, rather than reporting the whole
45
+ // object as an unrecognized relation query value.
46
+ if (isRecord(value) && '$lock' in value) {
47
+ throw new TypeError("'$lock' applies to the whole statement, not to a populated relation. Move it to the top level of the query.");
48
+ }
44
49
  if (isRelationQueryObject(value)) {
45
50
  return { query: value, required: value.$required === true, nested: true };
46
51
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
5
5
  "license": "MIT",
6
- "version": "0.26.2",
6
+ "version": "0.27.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -127,9 +127,9 @@
127
127
  "devDependencies": {
128
128
  "@libsql/client": "^0.17.4",
129
129
  "@neondatabase/serverless": "^1.1.0",
130
- "@nestjs/common": "^11.1.29",
131
- "@nestjs/core": "^11.1.29",
132
- "@nestjs/testing": "^11.1.29",
130
+ "@nestjs/common": "^11.2.1",
131
+ "@nestjs/core": "^11.2.1",
132
+ "@nestjs/testing": "^11.2.1",
133
133
  "@tursodatabase/database": "^0.7.2",
134
134
  "@tursodatabase/serverless": "^1.4.0",
135
135
  "@types/better-sqlite3": "^9.6.0",