uql-orm 0.43.0 → 0.44.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.
@@ -13,6 +13,12 @@ export type HttpQuerierDefaults = {
13
13
  * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.
14
14
  */
15
15
  readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;
16
+ /**
17
+ * The URL segment an entity is addressed by, defaulting to its kebab-cased class name - the same
18
+ * option the server handler takes, so one map serves both. State it where the default cannot: a
19
+ * build that minifies class names renames every route.
20
+ */
21
+ readonly entityPath?: (entity: Type<unknown>) => string;
16
22
  };
17
23
  export declare class HttpQuerier implements ClientQuerier {
18
24
  readonly basePath: string;
@@ -89,7 +89,7 @@ export class HttpQuerier {
89
89
  return remove(`${basePath}${qs}`, this.buildOptions(opts));
90
90
  }
91
91
  getBasePath(entity) {
92
- return `${this.basePath}/${entityPath(entity)}`;
92
+ return `${this.basePath}/${(this.defaults.entityPath ?? entityPath)(entity)}`;
93
93
  }
94
94
  read(path, q, opts) {
95
95
  if (this.defaults.readMethod === 'QUERY') {
@@ -1,4 +1,4 @@
1
- var d=[];function a(e){for(let t of d)t(e)}function q(e){d.push(e);let t=d.length-1;return()=>{d.splice(t,1)}}class g extends Error{status;constructor(e,t){super(e);this.status=t;this.name="RequestError"}}function l(e,t){return i(e,{method:"get"},t)}function E(e,t,n){let r=JSON.stringify(t);return i(e,{method:"post",body:r},n)}function f(e,t,n){let r=JSON.stringify(t);return i(e,{method:"patch",body:r},n)}function h(e,t,n){let r=JSON.stringify(t);return i(e,{method:"put",body:r},n)}function x(e,t){return i(e,{method:"delete"},t)}function m(e,t,n){let r=JSON.stringify(t);return i(e,{method:"QUERY",body:r},n)}function i(e,t,n){if(a({phase:"start",opts:n}),t.headers={accept:"application/json","content-type":"application/json",...n?.headers},n?.signal)t.signal=n.signal;return fetch(e,t).then((r)=>r.json().then((o)=>{if(r.status>=200&&r.status<300)return a({phase:"success",opts:n}),o;let P=o,c={message:P?.error?.message??r.statusText,code:P?.error?.code??r.status};throw a({phase:"error",error:c,opts:n}),new g(c.message,c.code)})).finally(()=>{a({phase:"complete",opts:n})})}function p(e){return e?Object.keys(e):[]}function S(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t!==Object.prototype&&t!==null}function O(e){let t=e.charAt(0).toLowerCase();for(let n=1;n<e.length;++n)t+=e[n]===e[n].toUpperCase()?"-"+e[n].toLowerCase():e[n];return t}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:""}},j=p(s),X=new Map(j.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function K(e){return O(e.name)}var T=["$select","$populate","$exclude","$where","$sort"],C=["$count"],b=["$skip","$limit"],k=["$candidates"],U=["$distinct"];var W=new Set([...T,...C,...b,...k,...U,"hardDelete","count"]);function u(e){if(!e)return"";let t=new URLSearchParams;for(let r of p(e)){let o=e[r];if(o===void 0)continue;t.append(r,typeof o==="object"&&o!==null?JSON.stringify(o):String(o))}let n=t.toString();return n?`?${n}`:""}function R(e,t){if(!S(t))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(t)}class Q{basePath;defaults;constructor(e,t={}){this.basePath=e;this.defaults=t}async findOneById(e,t,n,r){let o=this.getBasePath(e),y=u(n);return l(`${o}/${R(e,t)}${y}`,this.buildOptions(r))}findOne(e,t,n){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,t,n)}findMany(e,t,n){let r={...t};if(n?.count)r.count=!0;return this.read(this.getBasePath(e),r,n)}async findManyAndCount(e,t,n){let r=await this.findMany(e,t,{...n,count:!0});if(typeof r.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...r,count:r.count}}count(e,t,n){return this.read(`${this.getBasePath(e)}${s.count.path}`,t,n)}async exists(e,t,n){let r=await this.count(e,{...t,$limit:1},n);return{...r,data:r.data>0}}insertOne(e,t,n){let r=this.getBasePath(e);return E(r,t,this.buildOptions(n))}insertMany(e,t,n){let r=this.getBasePath(e);return E(`${r}${s.insertMany.path}`,t,this.buildOptions(n))}async updateOneById(e,t,n,r){let o=this.getBasePath(e);return f(`${o}/${R(e,t)}`,n,this.buildOptions(r))}updateMany(e,t,n,r){let o=this.getBasePath(e),y=u(t);return f(`${o}${y}`,n,this.buildOptions(r))}saveOne(e,t,n){let r=this.getBasePath(e);return h(r,t,this.buildOptions(n))}saveMany(e,t,n){let r=this.getBasePath(e);return h(`${r}${s.saveMany.path}`,t,this.buildOptions(n))}async deleteOneById(e,t,n={}){let r=this.getBasePath(e),o=n.hardDelete?u({hardDelete:n.hardDelete}):"";return x(`${r}/${R(e,t)}${o}`,this.buildOptions(n))}deleteMany(e,t,n={}){let r=this.getBasePath(e),o=u(n.hardDelete?{...t,hardDelete:n.hardDelete}:t);return x(`${r}${o}`,this.buildOptions(n))}getBasePath(e){return`${this.basePath}/${K(e)}`}read(e,t,n){if(this.defaults.readMethod==="QUERY")return m(e,t??{},this.buildOptions(n));return l(`${e}${u(t)}`,this.buildOptions(n))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var w={getQuerier:()=>new Q("/api")};function ne(e){w=e}function F(){return w}function re(){return F().getQuerier()}export{Q as HttpQuerier,g as RequestError,l as get,re as getQuerier,F as getQuerierPool,a as notify,q as on,f as patch,E as post,h as put,m as query,x as remove,ne as setQuerierPool};
1
+ var d=[];function a(e){for(let t of d)t(e)}function q(e){d.push(e);let t=d.length-1;return()=>{d.splice(t,1)}}class g extends Error{status;constructor(e,t){super(e);this.status=t;this.name="RequestError"}}function l(e,t){return i(e,{method:"get"},t)}function E(e,t,n){let r=JSON.stringify(t);return i(e,{method:"post",body:r},n)}function f(e,t,n){let r=JSON.stringify(t);return i(e,{method:"patch",body:r},n)}function h(e,t,n){let r=JSON.stringify(t);return i(e,{method:"put",body:r},n)}function x(e,t){return i(e,{method:"delete"},t)}function m(e,t,n){let r=JSON.stringify(t);return i(e,{method:"QUERY",body:r},n)}function i(e,t,n){if(a({phase:"start",opts:n}),t.headers={accept:"application/json","content-type":"application/json",...n?.headers},n?.signal)t.signal=n.signal;return fetch(e,t).then((r)=>r.json().then((o)=>{if(r.status>=200&&r.status<300)return a({phase:"success",opts:n}),o;let P=o,c={message:P?.error?.message??r.statusText,code:P?.error?.code??r.status};throw a({phase:"error",error:c,opts:n}),new g(c.message,c.code)})).finally(()=>{a({phase:"complete",opts:n})})}function p(e){return e?Object.keys(e):[]}function S(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t!==Object.prototype&&t!==null}function O(e){let t=e.charAt(0).toLowerCase();for(let n=1;n<e.length;++n)t+=e[n]===e[n].toUpperCase()?"-"+e[n].toLowerCase():e[n];return t}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:""}},j=p(s),X=new Map(j.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function K(e){return O(e.name)}var T=["$select","$populate","$exclude","$where","$sort"],C=["$count"],b=["$skip","$limit"],k=["$candidates"],w=["$distinct"];var W=new Set([...T,...C,...b,...k,...w,"hardDelete","count"]);function u(e){if(!e)return"";let t=new URLSearchParams;for(let r of p(e)){let o=e[r];if(o===void 0)continue;t.append(r,typeof o==="object"&&o!==null?JSON.stringify(o):String(o))}let n=t.toString();return n?`?${n}`:""}function R(e,t){if(!S(t))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(t)}class Q{basePath;defaults;constructor(e,t={}){this.basePath=e;this.defaults=t}async findOneById(e,t,n,r){let o=this.getBasePath(e),y=u(n);return l(`${o}/${R(e,t)}${y}`,this.buildOptions(r))}findOne(e,t,n){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,t,n)}findMany(e,t,n){let r={...t};if(n?.count)r.count=!0;return this.read(this.getBasePath(e),r,n)}async findManyAndCount(e,t,n){let r=await this.findMany(e,t,{...n,count:!0});if(typeof r.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...r,count:r.count}}count(e,t,n){return this.read(`${this.getBasePath(e)}${s.count.path}`,t,n)}async exists(e,t,n){let r=await this.count(e,{...t,$limit:1},n);return{...r,data:r.data>0}}insertOne(e,t,n){let r=this.getBasePath(e);return E(r,t,this.buildOptions(n))}insertMany(e,t,n){let r=this.getBasePath(e);return E(`${r}${s.insertMany.path}`,t,this.buildOptions(n))}async updateOneById(e,t,n,r){let o=this.getBasePath(e);return f(`${o}/${R(e,t)}`,n,this.buildOptions(r))}updateMany(e,t,n,r){let o=this.getBasePath(e),y=u(t);return f(`${o}${y}`,n,this.buildOptions(r))}saveOne(e,t,n){let r=this.getBasePath(e);return h(r,t,this.buildOptions(n))}saveMany(e,t,n){let r=this.getBasePath(e);return h(`${r}${s.saveMany.path}`,t,this.buildOptions(n))}async deleteOneById(e,t,n={}){let r=this.getBasePath(e),o=n.hardDelete?u({hardDelete:n.hardDelete}):"";return x(`${r}/${R(e,t)}${o}`,this.buildOptions(n))}deleteMany(e,t,n={}){let r=this.getBasePath(e),o=u(n.hardDelete?{...t,hardDelete:n.hardDelete}:t);return x(`${r}${o}`,this.buildOptions(n))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??K)(e)}`}read(e,t,n){if(this.defaults.readMethod==="QUERY")return m(e,t??{},this.buildOptions(n));return l(`${e}${u(t)}`,this.buildOptions(n))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var U={getQuerier:()=>new Q("/api")};function ne(e){U=e}function F(){return U}function re(){return F().getQuerier()}export{Q as HttpQuerier,g as RequestError,l as get,re as getQuerier,F as getQuerierPool,a as notify,q as on,f as patch,E as post,h as put,m as query,x as remove,ne as setQuerierPool};
2
2
 
3
- //# debugId=559C6B46B9C8DDFC64756E2164756E21
3
+ //# debugId=565BC245D18F9E8464756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -4,15 +4,15 @@
4
4
  "sourcesContent": [
5
5
  "import type { RequestCallback, RequestNotification } from '../type/index.js';\n\nconst subscriptors: RequestCallback[] = [];\n\nexport function notify(notification: RequestNotification): void {\n for (const subscriptor of subscriptors) {\n subscriptor(notification);\n }\n}\n\nexport function on(cb: RequestCallback): () => void {\n subscriptors.push(cb);\n const index = subscriptors.length - 1;\n return (): void => {\n subscriptors.splice(index, 1);\n };\n}\n",
6
6
  "import type { RequestErrorResponse } from '../../http/contract.js';\nimport type { RequestSuccessResponse } from '../../type/index.js';\nimport type { RequestOptions } from '../type/index.js';\nimport { notify } from './bus.js';\n\n/**\n * Error thrown for non-2xx responses. Carries the HTTP status so callers can key\n * behavior on it (401 redirects, 402 payment flows, error-boundary routing).\n */\nexport class RequestError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = 'RequestError';\n }\n}\n\nexport function get<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'get' }, opts);\n}\n\nexport function post<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'post', body }, opts);\n}\n\nexport function patch<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'patch', body }, opts);\n}\n\nexport function put<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'put', body }, opts);\n}\n\nexport function remove<T>(url: string, opts?: RequestOptions) {\n return request<T>(url, { method: 'delete' }, opts);\n}\n\n/**\n * HTTP QUERY (RFC 10008): a safe, idempotent read whose JSON query travels in the\n * request body, avoiding URL-length limits. Method name must stay uppercase\n * (fetch only normalizes the classic verbs).\n */\nexport function query<T>(url: string, payload: unknown, opts?: RequestOptions) {\n const body = JSON.stringify(payload);\n return request<T>(url, { method: 'QUERY', body }, opts);\n}\n\nfunction request<T>(url: string, init: RequestInit, opts?: RequestOptions) {\n notify({ phase: 'start', opts });\n\n init.headers = {\n accept: 'application/json',\n 'content-type': 'application/json',\n ...opts?.headers,\n };\n if (opts?.signal) {\n init.signal = opts.signal;\n }\n\n return fetch(url, init)\n .then((rawResp) =>\n rawResp.json().then((resp: unknown) => {\n const isSuccess = rawResp.status >= 200 && rawResp.status < 300;\n if (isSuccess) {\n notify({ phase: 'success', opts });\n return resp as RequestSuccessResponse<T>;\n }\n const errorResp = resp as Partial<RequestErrorResponse> | undefined;\n const error = {\n message: errorResp?.error?.message ?? rawResp.statusText,\n code: errorResp?.error?.code ?? rawResp.status,\n };\n notify({ phase: 'error', error, opts });\n throw new RequestError(error.message, error.code);\n }),\n )\n .finally(() => {\n notify({ phase: 'complete', opts });\n });\n}\n",
7
- "import type { EntityMeta, FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */\nexport function someValue(obj: object, pred: (value: unknown) => boolean): boolean {\n return someKey(obj, (key) => pred((obj as Record<string, unknown>)[key]));\n}\n\nconst isOperatorKey = (key: string) => key.startsWith('$');\n\n/**\n * Whether `value` is a non-empty object whose keys are query/update operators (`$eq`, `$push`, ...).\n * The single source of this test: the SQL dialects, the MongoDB dialect and the `$elemMatch` walker\n * all classify operator objects with it, and they used to disagree about `{}`.\n */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */\nexport function isOperatorOnlyObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));\n}\n\nexport function getKeys<T extends object>(obj: T): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\n/**\n * The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets\n * one, so the fallback is for a meta a decorator is still building - which is why the sites spelling\n * this 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\nexport function getFieldKeys<E>(fields: {\n [K in FieldKey<E>]?: FieldOptions;\n}): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\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",
7
+ "import type { EntityMeta, FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */\nexport function someValue(obj: object, pred: (value: unknown) => boolean): boolean {\n return someKey(obj, (key) => pred((obj as Record<string, unknown>)[key]));\n}\n\nconst isOperatorKey = (key: string) => key.startsWith('$');\n\n/**\n * Whether `value` is a non-empty object whose keys are query/update operators (`$eq`, `$push`, ...).\n * The single source of this test: the SQL dialects, the MongoDB dialect and the `$elemMatch` walker\n * all classify operator objects with it, and they used to disagree about `{}`.\n */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */\nexport function isOperatorOnlyObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));\n}\n\nexport function getKeys<T extends object>(obj: T): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\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\nexport function getFieldKeys<E>(fields: {\n [K in FieldKey<E>]?: FieldOptions;\n}): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\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",
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 RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
10
10
  "import type { FieldKey, IdKey, JsonFieldPaths, RelationKey, RelationTarget } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\nexport type QuerySelectOptions = {\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically add the prefix for the alias.\n */\n autoPrefixAlias?: boolean;\n};\n\n/**\n * Query field selection - `{ name: true }` whitelists specific fields. Fields only: a relation is a\n * sub-query rather than a projection flag, and a whitelist naming one could not say whether the\n * scalars come with it. Relations go in `$populate`.\n */\nexport type QuerySelect<E> = {\n [K in FieldKey<E>]?: BooleanLike;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E> = {\n [K in RelationKey<E>]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;\n};\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count. One statement per relation named here, batched over every parent at once, so it\n * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of\n * a relation of the same name that `$populate` filled with rows.\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\nexport type QueryCount<E> = {\n [K in ToManyRelationKey<E>]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = {\n [K in FieldKey<E>]?: true;\n};\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V> = (IsMany<V> extends true\n ? // `$lock` is statement-level, so it is excluded here rather than being silently ignored per\n // relation. `QueryUnique` is a `Pick` and already leaves it out.\n Except<Query<RelationTarget<V>>, '$lock'>\n : QueryUnique<RelationTarget<V>>) & {\n $required?: boolean;\n};\n\n/**\n * Ambient per-request context (e.g. `{ tenantId, userId, roles }`) resolved by parameterized\n * filters. Set with `withContext(ctx, cb)`. It's an `interface` (not a type alias) so you can type\n * your keys once via declaration merging and get them typed wherever context is read:\n *\n * ```ts\n * declare module 'uql-orm' {\n * interface UqlContext { tenantId: number; userId: string }\n * }\n * ```\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterCondition<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly condition: FilterCondition<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it.\n */\n readonly security?: boolean;\n /** What to do when the condition returns `undefined`. Defaults to `skip`, or `throw` for `security`. */\n readonly onMissing?: FilterOnMissing;\n};\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order\n * it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.\n */\ntype ToOneRelationKey<E> = { [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K }[RelationKey<E>];\n\n/** The relation names a parent holds many rows of, which a populated query fills with a list. */\ntype ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;\n\n/**\n * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,\n * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on\n * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:\n * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank\n * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.\n *\n * One mapped type over the three key sets rather than three intersected. The sets are disjoint - a\n * JSON path is dotted, and a field key cannot also be a relation key - and an assignability check\n * against an intersection is repeated per constituent, which made this the single most expensive\n * type in the package to check.\n */\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\nexport type QuerySortMap<E, Vector extends boolean = true> = {\n [K in FieldKey<E> | JsonFieldPaths<E> | RelationKey<E>]?: K extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its own size.\n IsMany<E[K]> extends true\n ? QuerySortByCount\n : QuerySortMap<RelationTarget<E[K]>, false>\n : K extends FieldKey<E>\n ? Vector extends true\n ? NonNullable<E[K]> extends readonly number[]\n ? QuerySortValue\n : QuerySortDirection\n : QuerySortDirection\n : QuerySortDirection;\n};\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E>;\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> = QueryFilter<E> & 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> = QueryPage<E> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * take a row-level lock on the rows this query returns (`SELECT ... FOR UPDATE`). Needs an open\n * transaction: outside one the statement commits and drops the lock before the caller can act on\n * the rows, so it is rejected rather than emitted. Locks only the queried entity, never anything\n * reached through `$populate`. SQL only; MongoDB and the SQLite family reject it.\n *\n * Declared here rather than on {@link QuerySearch}, which `update`/`delete` take: that placement\n * is what keeps the clause off those statements at the type level.\n */\n $lock?: QueryLock;\n\n /**\n * how many candidates an approximate-nearest-neighbour index explores before ranking, for a vector\n * search. Higher trades speed for recall; the default is whatever the engine's own is, which is\n * tuned for speed. Ignored where the search is exact (SQLite, libSQL and Turso scan every row) and\n * where the field carries no ANN index, since there is nothing to widen.\n *\n * The units are the index's, not UQL's, so the number is not comparable across index types: it\n * becomes `hnsw.ef_search` or `ivfflat.probes` on Postgres, `mhnsw_ef_search` on MariaDB, and\n * `numCandidates` on MongoDB Atlas. On Postgres it needs an open transaction, since a `SET LOCAL`\n * outside one applies to nothing.\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>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and\n * a validator checking a relation's own query both need, and what each used to enumerate for itself.\n * Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build\n * rather than the runtime if a clause is ever renamed.\n *\n * `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's\n * query accepts, so leaving it out is what excludes it from both.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes, never a relation's own query - the mirror of\n * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a\n * populated relation's rows are assembled after that, so there is nothing for a nested one to count.\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/**\n * options to get a single record.\n */\nexport type QueryOne<E> = Except<Query<E>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that decide a row's shape, captured from the query as written: the field names\n * `$select` and `$exclude` list, the value those maps carry (a falsy one subtracts instead of\n * selecting, as it does at runtime, and a widened map is how a projection that is not statically\n * known announces itself), and the relation names `$populate` lists.\n *\n * Each is captured as a *key set* rather than as the map itself, which is what keeps the checks\n * intact: TypeScript skips excess-property checking on a naked type parameter, so a captured map\n * would take a typo'd key without a word, while a captured key set makes that typo fail its own\n * `FieldKey<E>` / `RelationKey<E>` constraint. Every other clause - `$where`, `$sort`, and each\n * populated relation's own query - stays the concrete {@link Query} it is today.\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> = QueryStreamProjection<E, S, V, X, P> & {\n // Intersecting the captured names with {@link QueryCount}'s own leaves a to-one relation no key\n // here at all, so counting one is an excess property rather than a value to check. Narrowing the\n // key rather than the value also instantiates `QueryCount<E>` once instead of once per counted\n // relation, worth ~87k instantiations in a consuming project.\n $count?: { [K in C & keyof QueryCount<E>]?: QueryCount<E>[K] };\n};\n\n/**\n * {@link QueryProjection} without `$count`, which a stream cannot honor. Split out rather than\n * subtracted afterwards: an optional key is a *known* key even when its value maps over `never`, so\n * a statement that must not take the clause has to be built without it in the first place.\n * @internal\n */\ntype QueryStreamProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = {\n $select?: { [K in S]?: V } | readonly QueryRaw[];\n $exclude?: { [K in X]?: V };\n $populate?: { [K in P]?: QueryPopulate<E>[K] };\n};\n\n/**\n * A {@link QueryProjected} a stream can honor: no `$count`, which is batched over a result set a\n * stream never holds all of.\n */\nexport type QueryStreamProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = Except<\n Query<E>,\n '$count'\n> &\n QueryStreamProjection<E, S, V, X, P>;\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> = Query<E> & QueryProjection<E, S, V, X, P, C>;\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> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;\n\n/**\n * The keys a query comes back with, mirroring what the runtime projects: the fields a positive\n * `$select` names, or every field minus what a falsy `$select` entry or a truthy `$exclude` entry\n * subtracts, plus the relations `$populate` asked for. A positive `$select` wins outright, which is\n * why `$exclude` is only read on the branch where there is none.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P, C> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P\n // Populating a relation keeps the id whatever the projection says, and so does counting one: both\n // assemble their result by it (`selectFields` puts it back, as does MongoDB's `pipelineProjection`).\n | ([P | C] extends [never] ? never : NamedIdKey<E>);\n\n/**\n * The id key when it can be named, and nothing when it cannot: {@link IdKey} widens to *every* field\n * for an entity whose id is neither branded nor called `id`/`_id`/`uuid`, and adding that back would\n * hand the caller a row claiming fields the query never fetched. Missing an id costs a `$select`\n * entry; promising absent fields is the bug this type exists to prevent.\n * @internal\n */\ntype NamedIdKey<E> = [FieldKey<E>] extends [IdKey<E>] ? never : IdKey<E>;\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 row of a find result: the entity narrowed to the fields the query projected, plus the relations\n * it populated - reading anything the query left out is a compile error rather than a silent\n * `undefined`. Modifiers are preserved, so an optional field stays optional. Name a projected row\n * with it where a helper has to take one: `QueryFindResult<User, 'id' | 'name'>`.\n *\n * The entity itself when the query projects nothing, when it uses a raw-projection array (columns,\n * not fields), and when the projection is not uniform - a `Query<E>` built elsewhere, or a map\n * mixing selected and subtracted entries, whose positive keys inference cannot recover. Relations\n * keep their declared type: narrowing them means capturing their queries as maps, which costs those\n * queries their own checks.\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, C> & 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, C>, 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/**\n * result of an update operation.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the inserted IDs, in insertion order. Exact on `'returning'` dialects; inferred from the\n * driver header on the others (see {@link InsertIdSource}), and empty when the header\n * reports no generated ID.\n */\n ids?: PrimaryKey[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
11
11
  "import type { Query, QueryOptions } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\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 specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof Query<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof Query<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...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\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 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",
12
- "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityData,\n EntityId,\n FieldKey,\n IdValue,\n Query,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdatePayload,\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\n/**\n * The id as one path segment.\n *\n * A composite key has no spelling here yet - the route is `/:id`, and how several columns share one\n * segment is a serialization to invent rather than copy. Refused rather than interpolated, which\n * would have sent `[object Object]` for the server to reject. Its callers are `async` so this\n * surfaces as a rejection, like every other failure they can hand back.\n */\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>,\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>,\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>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: Query<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>,\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>, 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>, 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: EntityData<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(IdValue<E> | 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: UpdatePayload<E>,\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>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(IdValue<E> | undefined)[]>(`${basePath}${CRUD_ROUTES.saveMany.path}`, payload, this.buildOptions(opts));\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>, 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
+ "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityData,\n EntityId,\n FieldKey,\n IdValue,\n Query,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdatePayload,\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/**\n * The id as one path segment.\n *\n * A composite key has no spelling here yet - the route is `/:id`, and how several columns share one\n * segment is a serialization to invent rather than copy. Refused rather than interpolated, which\n * would have sent `[object Object]` for the server to reject. Its callers are `async` so this\n * surfaces as a rejection, like every other failure they can hand back.\n */\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>,\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>,\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>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: Query<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>,\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>, 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>, 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: EntityData<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(IdValue<E> | 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: UpdatePayload<E>,\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>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(IdValue<E> | undefined)[]>(`${basePath}${CRUD_ROUTES.saveMany.path}`, payload, this.buildOptions(opts));\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>, 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",
13
13
  "import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
14
14
  ],
15
- "mappings": "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",
16
- "debugId": "559C6B46B9C8DDFC64756E2164756E21",
15
+ "mappings": "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",
16
+ "debugId": "565BC245D18F9E8464756E2164756E21",
17
17
  "names": []
18
18
  }
@@ -1,4 +1,5 @@
1
1
  import { applyFilters, buildQueryWhereAsMap } from '../util/dialect.util.js';
2
+ import { entityName } from '../util/index.js';
2
3
  import { qualifyName } from '../util/sql.util.js';
3
4
  /**
4
5
  * The dialect's share of a pool's {@link ExtraOptions}: what changes the SQL rather than the
@@ -40,12 +41,10 @@ export class AbstractDialect {
40
41
  * nothing declared.
41
42
  */
42
43
  resolveTableAlias(meta) {
43
- const className = meta.entity.name;
44
- const name = meta.name ?? className;
45
- if (name !== className || !this.namingStrategy) {
46
- return name;
47
- }
48
- return this.namingStrategy.tableName(name);
44
+ // A name the author wrote is the table, verbatim: there is nothing to derive. Only a name that
45
+ // stood in for one is a derivation, which is what a naming strategy exists to make.
46
+ const name = entityName(meta);
47
+ return meta.derivedName && this.namingStrategy ? this.namingStrategy.tableName(name) : name;
49
48
  }
50
49
  /**
51
50
  * Where the table lives: {@link resolveTableAlias} behind its schema, when one applies. The schema
@@ -3,6 +3,9 @@ import type { FieldOptions, HookEvent, RelationOptions, Type } from '../../type/
3
3
  * What the member decorators record for one class, waiting for `@Entity()` or `defineEntity` to drain
4
4
  * it into the metadata registry. Member decorators receive no class reference under the standard
5
5
  * decorator spec, so this object is the only channel between them and the class decorator that does.
6
+ *
7
+ * The writable counterpart of `EntityMembers`, which is what registration reads: this one is written
8
+ * into member by member, so every map is present and none of them is readonly.
6
9
  */
7
10
  export type MemberRegistrations = {
8
11
  readonly fields: Record<string, FieldOptions>;
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
@@ -1,4 +1,4 @@
1
- import type { EntityId, EntityIndexInput, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, IdKey, RelationOptions, Type } from '../../type/index.js';
1
+ import type { EntityId, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, IdKey, RelationOptions, Type } from '../../type/index.js';
2
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
3
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
4
4
  export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
@@ -9,21 +9,17 @@ export declare function defineHook<E>(entity: Type<E>, methodName: string, event
9
9
  */
10
10
  export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E>, E>): EntityMeta<E>;
11
11
  export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
12
- /**
13
- * What a decorator bag and {@link EntityOptions} have in common at registration time. The keyed mapped
14
- * types in `EntityOptions<E>` are what check the imperative call; a member decorator has no class to key
15
- * against, so by the time either reaches the primitives the keys are plain strings.
16
- */
17
- type MemberSpecs = {
18
- readonly fields?: Readonly<Record<string, FieldOptions | undefined>>;
19
- readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
20
- readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
21
- };
22
12
  /**
23
13
  * Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
24
14
  * API converge on one registration path before anything is finalized.
25
15
  */
26
- export declare function applyMembers<E>(entity: Type<E>, specs: MemberSpecs | undefined): void;
16
+ export declare function applyMembers<E>(entity: Type<E>, specs: EntityMembers | undefined): void;
17
+ /**
18
+ * Registers an entity described by data alone, minting the class the registry keys it by. The row
19
+ * type follows from the spec - see {@link SpecRow} - so a definition written out is checked column by
20
+ * column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
21
+ * cannot describe, such as the interface `uql-migrate types` generated for it.
22
+ */
27
23
  export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
28
24
  /**
29
25
  * Refuses an entity whose primary key is not one column, naming the path that cannot express it.
@@ -42,6 +38,12 @@ export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>
42
38
  * one of its columns would address every row agreeing on that one.
43
39
  */
44
40
  export declare function idOf<E>(meta: EntityMeta<E>, row: E): EntityId<E>;
41
+ /**
42
+ * Forgets an entity, and reports whether there was one - for a registry that grows at runtime, where a
43
+ * deleted content type would otherwise keep its metadata for the life of the process. Nothing rewrites
44
+ * what pointed at it, and a decorated class does not come back (its decorators drained at first
45
+ * registration). See the Runtime Schemas guide.
46
+ */
47
+ export declare function removeEntity<E>(entity: Type<E>): boolean;
45
48
  export declare function getEntities(): Type<unknown>[];
46
49
  export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
47
- export {};
@@ -1,14 +1,19 @@
1
1
  import { SOFT_DELETE_FILTER } from '../../type/index.js';
2
- import { fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
2
+ import { entityName, fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
3
3
  import { ownRegistrations } from '../decorator/bag.js';
4
- // Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
5
- // evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
6
- // because v1 changed the `FieldOptions` shape: a tree holding both majors gets two maps rather than one
7
- // map with entries the other major cannot read.
8
- const holder = globalThis;
9
- const metaKey = Symbol.for('uql-orm/entity/metadata/v1');
10
- const metas = holder[metaKey] ?? new Map();
11
- holder[metaKey] = metas;
4
+ /**
5
+ * A map held on `globalThis` through the global symbol registry, so a single one survives multiple
6
+ * evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). The keys are
7
+ * version-suffixed because v1 changed the `FieldOptions` shape: a tree holding both majors gets two
8
+ * maps rather than one map with entries the other major cannot read.
9
+ */
10
+ function globalMap(key) {
11
+ const holder = globalThis;
12
+ const symbol = Symbol.for(key);
13
+ holder[symbol] ??= new Map();
14
+ return holder[symbol];
15
+ }
16
+ const metas = globalMap('uql-orm/entity/metadata/v1');
12
17
  export function defineField(entity, key, opts = {}) {
13
18
  const meta = ensureWritableMeta(entity);
14
19
  if (!opts.type && !opts.references && !opts.virtual) {
@@ -107,6 +112,12 @@ export function applyMembers(entity, specs) {
107
112
  }
108
113
  }
109
114
  }
115
+ /**
116
+ * Registers an entity described by data alone, minting the class the registry keys it by. The row
117
+ * type follows from the spec - see {@link SpecRow} - so a definition written out is checked column by
118
+ * column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
119
+ * cannot describe, such as the interface `uql-migrate types` generated for it.
120
+ */
110
121
  export function defineEntity(entity, opts = {}) {
111
122
  // Ahead of any registration, so a rejected definition leaves nothing half-written in the registry.
112
123
  // A dotted name reads like a schema and is not one: it escapes as a single identifier, so the
@@ -129,15 +140,26 @@ export function defineEntity(entity, opts = {}) {
129
140
  for (const index of opts.indexes ?? []) {
130
141
  defineIndex(entity, index);
131
142
  }
132
- for (const [name, spec] of Object.entries(opts.filters ?? {})) {
133
- if (spec)
134
- defineFilter(entity, name, spec);
143
+ for (const [name, filter] of Object.entries(opts.filters ?? {})) {
144
+ if (filter)
145
+ defineFilter(entity, name, filter);
135
146
  }
136
147
  if (!hasKeys(meta.fields)) {
137
148
  throw TypeError(`'${entity.name}' must have fields`);
138
149
  }
139
- // A later call composes onto the entity, so saying nothing about the table retracts nothing.
140
- meta.name = opts.name ?? meta.name ?? entity.name;
150
+ // A later call composes onto the entity, so saying nothing about the table retracts nothing - which
151
+ // is why `derivedName` is only ever *set*, never recomputed from what a previous call left.
152
+ // It records that the class name stood in, telling a naming strategy there is something to derive;
153
+ // comparing the two cannot, since an entity may name its table exactly what its class is called and
154
+ // a spec's minted class is named after its table.
155
+ if (opts.name !== undefined) {
156
+ meta.name = opts.name;
157
+ meta.derivedName = false;
158
+ }
159
+ else if (meta.name === undefined) {
160
+ meta.name = entity.name;
161
+ meta.derivedName = true;
162
+ }
141
163
  meta.schema = opts.schema ?? meta.schema;
142
164
  let proto = Object.getPrototypeOf(entity.prototype);
143
165
  while (proto.constructor !== Object) {
@@ -208,8 +230,17 @@ export function idOf(meta, row) {
208
230
  }
209
231
  return Object.fromEntries(ids.map((key) => [key, row[key]]));
210
232
  }
233
+ /**
234
+ * Forgets an entity, and reports whether there was one - for a registry that grows at runtime, where a
235
+ * deleted content type would otherwise keep its metadata for the life of the process. Nothing rewrites
236
+ * what pointed at it, and a decorated class does not come back (its decorators drained at first
237
+ * registration). See the Runtime Schemas guide.
238
+ */
239
+ export function removeEntity(entity) {
240
+ return metas.delete(entity);
241
+ }
211
242
  export function getEntities() {
212
- return [...metas.entries()].reduce((acc, [key, val]) => {
243
+ return metas.entries().reduce((acc, [key, val]) => {
213
244
  if (val.ids.length) {
214
245
  acc.push(key);
215
246
  }
@@ -391,7 +422,7 @@ function fillForeignKeyRelations(meta) {
391
422
  }
392
423
  /** `<entityName><IdColumn>`, not the `<relationKey>Id` an owning to-one derives: a junction row has no relation key to borrow from. */
393
424
  function junctionColumn(meta, idKey) {
394
- return lowerFirst(meta.name ?? '') + upperFirst(meta.fields[idKey]?.name ?? idKey);
425
+ return lowerFirst(entityName(meta)) + upperFirst(meta.fields[idKey]?.name ?? idKey);
395
426
  }
396
427
  /** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
397
428
  const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
@@ -51,6 +51,14 @@ export type ResponseHook<Ctx = unknown> = <E extends object>(ctx: HookContext<E,
51
51
  export type RequestHandlerOptions<Ctx = unknown> = {
52
52
  include?: Type<any>[];
53
53
  exclude?: Type<any>[];
54
+ /**
55
+ * The URL segment an entity is addressed by, defaulting to its kebab-cased class name.
56
+ *
57
+ * State it where the default cannot serve: a build that minifies class names renames every route,
58
+ * and two entities mapping one table in different schemas collide on one. The browser client takes
59
+ * the same option, so both ends can read one map.
60
+ */
61
+ entityPath?: (entity: Type<unknown>) => string;
54
62
  /**
55
63
  * Allow augment any kind of request before it runs. Hooks may be async
56
64
  * and abort the request by throwing (a numeric `status` on the error is honored).
@@ -9,6 +9,7 @@ function tableOf(entity) {
9
9
  }
10
10
  export function createRequestHandler(opts) {
11
11
  const { include, exclude, pre, preSave, preFilter, post, getContext, pool } = opts;
12
+ const pathOf = opts.entityPath ?? entityPath;
12
13
  let entities = include ?? getEntities();
13
14
  if (exclude) {
14
15
  entities = entities.filter((entity) => !exclude.includes(entity));
@@ -16,15 +17,14 @@ export function createRequestHandler(opts) {
16
17
  if (!entities.length) {
17
18
  throw new TypeError('no entities for the uql middleware');
18
19
  }
19
- // The route is the class name, so two entities mapping one table in different schemas collide here
20
- // even though nothing else about them does. All of them at once, so fixing the first collision
21
- // does not just reveal the next.
22
- const byPath = Map.groupBy(entities, entityPath);
20
+ // All of them at once, so fixing the first collision does not just reveal the next.
21
+ const byPath = Map.groupBy(entities, pathOf);
23
22
  const collisions = [...byPath].filter(([, clashing]) => clashing.length > 1);
24
23
  if (collisions.length) {
25
24
  const lines = collisions.map(([path, clashing]) => ` /${path} <- ${clashing.map(tableOf).join(', ')}`);
26
25
  throw new TypeError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
27
- "A route is the kebab-cased class name. Rename a class, or pass only one of them in 'include'.");
26
+ "A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
27
+ "pass an 'entityPath', or pass only one of them in 'include'.");
28
28
  }
29
29
  // oxlint-disable-next-line typescript/no-explicit-any -- heterogeneous entity map
30
30
  const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
@@ -14,6 +14,11 @@ export declare function runStatus(migrator: Migrator): Promise<void>;
14
14
  export declare function runPending(migrator: Migrator): Promise<void>;
15
15
  export declare function runGenerate(migrator: Migrator, args: string[]): Promise<void>;
16
16
  export declare function runGenerateFromEntities(migrator: Migrator, args: string[]): Promise<void>;
17
+ /**
18
+ * Writes a `.d.ts` for the registered entities. The point is a schema defined at runtime: the same
19
+ * registration that made the tables is what the compiler then checks queries against.
20
+ */
21
+ export declare function runTypes(migrator: Migrator, args: string[]): void;
17
22
  export declare function runSync(migrator: Migrator, args: string[], config: Partial<Config>): Promise<void>;
18
23
  export declare function runGenerateFromDb(migrator: Migrator, args: string[], config: Partial<Config>): Promise<void>;
19
24
  export declare function runDriftCheck(migrator: Migrator, config: Partial<Config>): Promise<void>;
@@ -4,6 +4,7 @@ import * as path from 'node:path';
4
4
  import { assertCliConfig } from './assertCliConfig.js';
5
5
  import { loadConfig } from './cli-config.js';
6
6
  import { createEntityCodeGenerator } from './codegen/entityCodeGenerator.js';
7
+ import { entityTypesSource } from './codegen/entityTypes.js';
7
8
  import { detectDrift } from './drift/driftDetector.js';
8
9
  import { Migrator } from './migrator.js';
9
10
  import { buildEntityAST, createSchemaGenerator } from './schemaGenerator.js';
@@ -74,6 +75,9 @@ export async function main(args = process.argv.slice(2)) {
74
75
  case 'sync':
75
76
  await runSync(migrator, filteredArgs.slice(1), config);
76
77
  break;
78
+ case 'types':
79
+ runTypes(migrator, filteredArgs.slice(1));
80
+ break;
77
81
  case 'pending':
78
82
  await runPending(migrator);
79
83
  break;
@@ -189,35 +193,45 @@ export async function runGenerateFromEntities(migrator, args) {
189
193
  const filePath = await migrator.generateFromEntities(name);
190
194
  console.log(`\nCreated migration from entities: ${filePath}`);
191
195
  }
196
+ /**
197
+ * Writes a `.d.ts` for the registered entities. The point is a schema defined at runtime: the same
198
+ * registration that made the tables is what the compiler then checks queries against.
199
+ */
200
+ export function runTypes(migrator, args) {
201
+ const output = readOutput(args) ?? './uql-entities.d.ts';
202
+ fs.mkdirSync(path.dirname(output), { recursive: true });
203
+ fs.writeFileSync(output, entityTypesSource(migrator.entities), 'utf-8');
204
+ console.log(`Wrote ${migrator.entities.length} entities to ${output}`);
205
+ }
206
+ /** `--output`/`-o`, wherever a command takes one. */
207
+ function readOutput(args) {
208
+ const at = args.findIndex((arg) => arg === '--output' || arg === '-o');
209
+ return at === -1 ? undefined : args[at + 1];
210
+ }
192
211
  export async function runSync(migrator, args, config) {
193
- if (args.includes('--force')) {
194
- console.log('\n⚠️ WARNING: This will drop and recreate all tables!');
195
- console.log(' All data will be lost. This should only be used in development.\n');
196
- await migrator.sync({ force: true });
197
- console.log('\nSchema sync completed.');
198
- return;
199
- }
200
212
  // Pulling the database into entity files is what `generate:from-db` does; one implementation.
201
213
  if (args.includes('--pull')) {
202
214
  return runGenerateFromDb(migrator, args, config);
203
215
  }
216
+ const force = args.includes('--force');
204
217
  const safe = !args.includes('--unsafe');
218
+ const options = { force, safe, drop: !safe };
219
+ // Ahead of the warning as well as of the run: `--dry-run` means the same thing whatever else was
220
+ // asked for, and it used to be ignored beside `--force`.
205
221
  if (args.includes('--dry-run')) {
206
- const statements = await migrator.planSync({ safe, drop: !safe });
222
+ const statements = await migrator.planSync(options);
207
223
  console.log(statements.length ? `\n${statements.join('\n')}` : '\nSchema is already in sync.');
208
224
  return;
209
225
  }
210
- await migrator.autoSync({ safe, drop: !safe, logging: true });
226
+ if (force) {
227
+ console.log('\n⚠️ WARNING: This will drop and recreate all tables!');
228
+ console.log(' All data will be lost. This should only be used in development.\n');
229
+ }
230
+ await migrator.sync({ ...options, logging: true });
211
231
  console.log('\nSchema sync completed.');
212
232
  }
213
233
  export async function runGenerateFromDb(migrator, args, config) {
214
- // Parse output directory
215
- let outputDir = './src/entities';
216
- for (let i = 0; i < args.length; i++) {
217
- if ((args[i] === '--output' || args[i] === '-o') && args[i + 1]) {
218
- outputDir = args[++i];
219
- }
220
- }
234
+ const outputDir = readOutput(args) ?? './src/entities';
221
235
  if (!migrator.schemaIntrospector) {
222
236
  console.error('No introspector available. Check your pool configuration.');
223
237
  process.exit(1);
@@ -342,6 +356,9 @@ Commands:
342
356
  --pull Go the other way: generate entities from the database
343
357
  --force Drop and recreate all tables (dangerous!)
344
358
 
359
+ types Write a .d.ts for the registered entities
360
+ --output, -o <file> Output path (default: ./uql-entities.d.ts)
361
+
345
362
  drift:check Check for schema drift between entities and database
346
363
 
347
364
  Configuration:
@@ -0,0 +1,7 @@
1
+ import type { Type } from '../../type/index.js';
2
+ /**
3
+ * A `.d.ts` for the entities as registered, so a shape that only exists at runtime reaches the
4
+ * compiler too. Interfaces, not the entity classes `generate:from-db` writes: those are the source of
5
+ * a schema, these describe one already defined elsewhere.
6
+ */
7
+ export declare function entityTypesSource(entities: readonly Type<unknown>[]): string;
@@ -0,0 +1,69 @@
1
+ import { getMeta } from '../../entity/index.js';
2
+ import { canonicalToTypeScript } from '../../schema/canonicalType.js';
3
+ import { resolveColumnCanonicalType } from '../../schema/schemaASTBuilder.js';
4
+ import { isToManyRelation, upperFirst } from '../../util/index.js';
5
+ /**
6
+ * A `.d.ts` for the entities as registered, so a shape that only exists at runtime reaches the
7
+ * compiler too. Interfaces, not the entity classes `generate:from-db` writes: those are the source of
8
+ * a schema, these describe one already defined elsewhere.
9
+ */
10
+ export function entityTypesSource(entities) {
11
+ const metas = [...entities]
12
+ .map((entity) => getMeta(entity))
13
+ .sort((a, b) => a.entity.name.localeCompare(b.entity.name));
14
+ const names = interfaceNames(metas);
15
+ const interfaces = metas.map((meta) => {
16
+ const members = [
17
+ ...Object.entries(meta.fields).map(([key, field]) => member(key, fieldType(field))),
18
+ ...Object.entries(meta.relations).map(([key, rel]) => member(key, relationType(rel, names))),
19
+ ];
20
+ return `export interface ${names.get(meta.entity)} {\n${members.join('\n')}\n}`;
21
+ });
22
+ return ['// Generated by `uql-migrate types`. Do not edit.', '', ...interfaces, ''].join('\n');
23
+ }
24
+ /**
25
+ * One interface name per entity, settled before anything is written so a relation and its target
26
+ * agree. A class minted at runtime is named after the content type it came from, which nothing stops
27
+ * from carrying a dash or colliding with another once the dashes are gone - and either would emit a
28
+ * file that does not compile.
29
+ */
30
+ function interfaceNames(metas) {
31
+ const taken = new Set();
32
+ return new Map(metas.map((meta) => {
33
+ const base = identifier(upperFirst(meta.entity.name));
34
+ let name = base;
35
+ for (let n = 2; taken.has(name); n++) {
36
+ name = `${base}${n}`;
37
+ }
38
+ taken.add(name);
39
+ return [meta.entity, name];
40
+ }));
41
+ }
42
+ /** `text` as an identifier: what cannot be in one is dropped, and what cannot start one is prefixed. */
43
+ function identifier(text) {
44
+ const stripped = text.replace(/[^A-Za-z0-9_$]/g, '');
45
+ return /^[A-Za-z_$]/.test(stripped) ? stripped : `Entity${stripped}`;
46
+ }
47
+ /** A column name a property cannot hold - `hero-image` - is quoted rather than dropped. */
48
+ function member(key, type) {
49
+ const name = key === identifier(key) ? key : JSON.stringify(key);
50
+ return ` ${name}?: ${type};`;
51
+ }
52
+ /**
53
+ * The property type a column reads back as, resolved the way the DDL resolves it - so a foreign key
54
+ * reports the type of the key it points at rather than the fallback its own options carry.
55
+ */
56
+ function fieldType(field) {
57
+ return field ? canonicalToTypeScript(resolveColumnCanonicalType(field)) : 'unknown';
58
+ }
59
+ /**
60
+ * A relation is the related interface, a list where the cardinality says so - and `unknown` where the
61
+ * target is outside the set, since naming an interface the file does not declare would not compile.
62
+ */
63
+ function relationType(relation, names) {
64
+ const target = relation && names.get(relation.entity());
65
+ if (!target) {
66
+ return 'unknown';
67
+ }
68
+ return isToManyRelation(relation) ? `${target}[]` : target;
69
+ }
@@ -4,4 +4,5 @@
4
4
  * Generates TypeScript entity code from database schemas.
5
5
  */
6
6
  export { createEntityCodeGenerator, EntityCodeGenerator, type EntityCodeGeneratorOptions, type GeneratedEntity, } from './entityCodeGenerator.js';
7
+ export { entityTypesSource } from './entityTypes.js';
7
8
  export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, type SqlMigrationModuleOptions, } from './migrationFile.js';
@@ -5,4 +5,5 @@
5
5
  */
6
6
  // Entity code generator
7
7
  export { createEntityCodeGenerator, EntityCodeGenerator, } from './entityCodeGenerator.js';
8
+ export { entityTypesSource } from './entityTypes.js';
8
9
  export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, } from './migrationFile.js';
@@ -1,4 +1,4 @@
1
- export type { ColumnSchema, DialectName, ForeignKeySchema, IndexSchema, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, SchemaDiff, SchemaGenerator, SchemaIntrospector, SqlDialectName, SqlQuerier, SqlQueryDialect, TableSchema, } from '../type/index.js';
1
+ export type { ColumnSchema, DialectName, ForeignKeySchema, IndexSchema, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, SchemaDiff, SchemaGenerator, SchemaIntrospector, SqlDialectName, SqlQuerier, SqlQueryDialect, SyncOptions, TableSchema, } from '../type/index.js';
2
2
  export { type Config, isSqlQuerier } from '../type/index.js';
3
3
  export { acquireQuerierForMigrations } from './acquireQuerierForMigrations.js';
4
4
  export { assertCliConfig } from './assertCliConfig.js';
@@ -1,4 +1,4 @@
1
- import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, Type } from '../type/index.js';
1
+ import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, SyncOptions, Type } from '../type/index.js';
2
2
  import { LoggerWrapper } from '../util/index.js';
3
3
  import type { IMigrationBuilder } from './builder/types.js';
4
4
  /**
@@ -88,31 +88,45 @@ export declare class Migrator {
88
88
  private introspectClaimedSchemas;
89
89
  findEntityForTable(tableName: string): Promise<Type<unknown> | undefined>;
90
90
  /**
91
- * Sync schema directly (for development only - not for production!)
92
- */
93
- sync(options?: {
94
- force?: boolean;
95
- }): Promise<void>;
96
- /**
97
- * Drops and recreates all tables (Development only!)
91
+ * Applies the entity schema to the database: every registered entity, or the one `entity` names.
92
+ *
93
+ * The whole surface is this and {@link planSync}, which answers the same question without running
94
+ * it - `force` and a single entity included, so `--dry-run` means the same thing whatever else was
95
+ * asked for.
98
96
  */
99
- syncForce(): Promise<void>;
97
+ sync(options?: SyncOptions): Promise<void>;
100
98
  /**
101
- * Safely synchronizes the schema by only adding missing tables and columns.
99
+ * Every table dropped and recreated.
100
+ *
101
+ * Both directions span the whole entity set rather than looping an entity at a time. A per-entity
102
+ * AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
103
+ * with no referential integrity; and the old drop loop went in reverse *declaration* order, which
104
+ * says nothing about the relation graph and is rejected as soon as the constraints are really there.
102
105
  */
103
- autoSync(options?: {
104
- safe?: boolean;
105
- drop?: boolean;
106
- logging?: boolean;
107
- }): Promise<void>;
106
+ private forceStatements;
108
107
  /**
109
- * The DDL {@link autoSync} would run, without running it. Separate so `--dry-run` shows the real
108
+ * Sync one entity, for a schema that grows while the process runs: a content type an admin just
109
+ * created is one table to add, where the whole set would read the catalogue to work that out.
110
+ *
111
+ * The new-table case costs one existence check and creates with `IF NOT EXISTS`, so instances racing
112
+ * the same admin save settle instead of colliding. An existing table still pays for introspection,
113
+ * since a column diff needs the columns.
114
+ */
115
+ /** The DDL for one entity: {@link planSync} narrowed to the table it names. */
116
+ private planEntity;
117
+ /** An alter diff as statements, narrowed to what the caller allows. */
118
+ private alterFromDiff;
119
+ /** The same for one entity against the table it already has, and nothing where the two agree. */
120
+ private alterFromEntity;
121
+ /** The configured entities, with `entity` among them however the migrator was built. */
122
+ private entitiesWith;
123
+ /** The introspector for a claimed schema, which is the connection's own where none is claimed. */
124
+ private introspectorFor;
125
+ /**
126
+ * The DDL {@link sync} would run, without running it. Separate so `--dry-run` shows the real
110
127
  * statements rather than a summary of a second, differently-computed diff.
111
128
  */
112
- planSync(options?: {
113
- safe?: boolean;
114
- drop?: boolean;
115
- }): Promise<string[]>;
129
+ planSync(options?: SyncOptions): Promise<string[]>;
116
130
  /**
117
131
  * New tables are emitted together, never one at a time: a single-entity AST has no other table for a
118
132
  * relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
@@ -305,7 +305,7 @@ export class Migrator {
305
305
  const claimed = new Set(this.entities.map((entity) => this.pool.dialect.resolveSchema(getMeta(entity))));
306
306
  const merged = new SchemaAST();
307
307
  for (const schema of claimed) {
308
- const introspector = schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
308
+ const introspector = this.introspectorFor(schema);
309
309
  if (!introspector) {
310
310
  continue;
311
311
  }
@@ -330,52 +330,90 @@ export class Migrator {
330
330
  return undefined;
331
331
  }
332
332
  /**
333
- * Sync schema directly (for development only - not for production!)
333
+ * Applies the entity schema to the database: every registered entity, or the one `entity` names.
334
+ *
335
+ * The whole surface is this and {@link planSync}, which answers the same question without running
336
+ * it - `force` and a single entity included, so `--dry-run` means the same thing whatever else was
337
+ * asked for.
334
338
  */
335
339
  async sync(options = {}) {
336
- if (options.force) {
337
- return this.syncForce();
340
+ const statements = await this.planSync(options);
341
+ if (statements.length) {
342
+ await this.executeSyncStatements(statements, options);
343
+ }
344
+ else if (options.logging) {
345
+ this.logger.logSchema('Schema is already in sync.');
338
346
  }
339
- return this.autoSync({ safe: true });
340
347
  }
341
348
  /**
342
- * Drops and recreates all tables (Development only!)
349
+ * Every table dropped and recreated.
350
+ *
351
+ * Both directions span the whole entity set rather than looping an entity at a time. A per-entity
352
+ * AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
353
+ * with no referential integrity; and the old drop loop went in reverse *declaration* order, which
354
+ * says nothing about the relation graph and is rejected as soon as the constraints are really there.
343
355
  */
344
- async syncForce() {
345
- await this.ensureSchemaGenerator();
346
- // Both directions span the whole entity set rather than looping an entity at a time. A per-entity
347
- // AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
348
- // with no referential integrity; and the old drop loop went in reverse *declaration* order, which
349
- // says nothing about the relation graph and is rejected as soon as the constraints are really there.
350
- const statements = [
356
+ forceStatements() {
357
+ return [
351
358
  ...this.generator.generateDropSchema(this.entities, { ifExists: true, cascade: true }),
352
359
  ...this.generator.generateCreateSchema(this.entities),
353
360
  ];
354
- await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(async () => {
355
- for (const sql of statements) {
356
- this.logger.logSchema(`Executing: ${sql}`);
357
- await querier.run(sql);
358
- }
359
- }));
360
- this.logger.logSchema('Schema sync (force) completed');
361
361
  }
362
362
  /**
363
- * Safely synchronizes the schema by only adding missing tables and columns.
363
+ * Sync one entity, for a schema that grows while the process runs: a content type an admin just
364
+ * created is one table to add, where the whole set would read the catalogue to work that out.
365
+ *
366
+ * The new-table case costs one existence check and creates with `IF NOT EXISTS`, so instances racing
367
+ * the same admin save settle instead of colliding. An existing table still pays for introspection,
368
+ * since a column diff needs the columns.
364
369
  */
365
- async autoSync(options = {}) {
366
- const statements = await this.planSync(options);
367
- if (statements.length === 0) {
368
- if (options.logging)
369
- this.logger.logSchema('Schema is already in sync.');
370
- return;
370
+ /** The DDL for one entity: {@link planSync} narrowed to the table it names. */
371
+ async planEntity(entity, options) {
372
+ const meta = getMeta(entity);
373
+ // Before anything reads `this.generator`, whose own failure names neither the entity nor the
374
+ // dialect that has no support.
375
+ const introspector = this.introspectorFor(this.pool.dialect.resolveSchema(meta));
376
+ if (!introspector) {
377
+ throw new TypeError(`No introspector for '${meta.entity.name}' on '${this.dialectName}'`);
371
378
  }
372
- await this.executeSyncStatements(statements, options);
379
+ const tableName = this.generator.resolveTableName(meta);
380
+ return (await introspector.tableExists(tableName))
381
+ ? this.alterFromEntity(entity, (await introspectSchema(introspector)).getTable(tableName), options)
382
+ : // Spanning the whole set, so a foreign key resolves against the tables it points at, and
383
+ // always including this entity: pinned to an explicit `entities` list, a sync of one outside
384
+ // it emitted nothing at all. `only` is what keeps the statements to this table.
385
+ this.generator.generateCreateSchema(this.entitiesWith(entity), { only: [tableName], ifNotExists: true });
386
+ }
387
+ /** An alter diff as statements, narrowed to what the caller allows. */
388
+ alterFromDiff(diff, options) {
389
+ return this.generator.generateAlterTable(this.filterDiff(diff, options));
390
+ }
391
+ /** The same for one entity against the table it already has, and nothing where the two agree. */
392
+ alterFromEntity(entity, table, options) {
393
+ const diff = this.generator.diffSchema(entity, table);
394
+ return diff?.type === 'alter' ? this.alterFromDiff(diff, options) : [];
395
+ }
396
+ /** The configured entities, with `entity` among them however the migrator was built. */
397
+ entitiesWith(entity) {
398
+ const entities = this.entities;
399
+ return entities.includes(entity) ? entities : [...entities, entity];
400
+ }
401
+ /** The introspector for a claimed schema, which is the connection's own where none is claimed. */
402
+ introspectorFor(schema) {
403
+ return schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
373
404
  }
374
405
  /**
375
- * The DDL {@link autoSync} would run, without running it. Separate so `--dry-run` shows the real
406
+ * The DDL {@link sync} would run, without running it. Separate so `--dry-run` shows the real
376
407
  * statements rather than a summary of a second, differently-computed diff.
377
408
  */
378
409
  async planSync(options = {}) {
410
+ await this.ensureSchemaGenerator();
411
+ if (options.force) {
412
+ return this.forceStatements();
413
+ }
414
+ if (options.entity) {
415
+ return this.planEntity(options.entity, options);
416
+ }
379
417
  const creating = [];
380
418
  const altering = [];
381
419
  for (const { diff, entity } of await this.pendingDiffs()) {
@@ -384,7 +422,7 @@ export class Migrator {
384
422
  creating.push(diff.tableName);
385
423
  }
386
424
  else if (diff.type === 'alter') {
387
- altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
425
+ altering.push(...this.alterFromDiff(diff, options));
388
426
  }
389
427
  }
390
428
  return [...this.createSchema(creating), ...altering];
@@ -452,10 +490,15 @@ export class Migrator {
452
490
  return filteredDiff;
453
491
  }
454
492
  async executeSyncStatements(statements, options) {
455
- // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one.
456
- await withQuerierForMigrations(this.pool, (querier) => this.dialectName === 'mongodb'
457
- ? this.executeMongoSyncStatements(statements, options, querier)
458
- : querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
493
+ // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one -
494
+ // and asks for a SQL querier before it opens it, since `transaction` is what a Mongo one lacks and
495
+ // reaching for it first reports that instead of which querier the dialect needs.
496
+ if (this.dialectName === 'mongodb') {
497
+ await withQuerierForMigrations(this.pool, (querier) => this.executeMongoSyncStatements(statements, options, querier));
498
+ }
499
+ else {
500
+ await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
501
+ }
459
502
  if (options.logging)
460
503
  this.logger.logSchema('Schema synchronization completed');
461
504
  }
@@ -5,10 +5,11 @@
5
5
  * - Entity metadata (decorator-based entities)
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
- import type { EntityMeta, FieldOptions, Type } from '../type/index.js';
8
+ import type { EntityGetter } from '../type/entity.js';
9
+ import type { EntityMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
9
10
  import type { NamingStrategy } from '../type/namingStrategy.js';
10
11
  import { SchemaAST } from './schemaAST.js';
11
- import { type ForeignKeyAction } from './types.js';
12
+ import { type CanonicalType, type ForeignKeyAction } from './types.js';
12
13
  /**
13
14
  * Options for building SchemaAST from entities.
14
15
  */
@@ -31,3 +32,23 @@ export interface BuildSchemaASTOptions {
31
32
  * resolves against a table another entity declares, and an index against the columns of its own.
32
33
  */
33
34
  export declare function buildSchemaAST(entities: readonly Type<unknown>[], options?: BuildSchemaASTOptions): SchemaAST;
35
+ /**
36
+ * Resolve the canonical type for a field, inheriting from the referenced
37
+ * entity's primary key when the field is a foreign-key reference
38
+ * (`@Field({ references: () => SomeEntity })`) with no explicit type of its
39
+ * own.
40
+ *
41
+ * Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
42
+ * `string`, erased at runtime) falls back to the generic string inference in
43
+ * {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
44
+ * foreign key column whose type doesn't match the UUID primary key it
45
+ * references, which Postgres (and most databases) reject outright.
46
+ *
47
+ * `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
48
+ * is what distinguishes "no type was given" from "the decorator explicitly set
49
+ * a type" - including explicit constructor overrides like `type: BigInt`, which
50
+ * a value-based check (e.g. `typeof field.type === 'string'`) would miss since
51
+ * reflection also produces constructor values like `String`/`Number`.
52
+ * `columnType` remains the unambiguous, always-respected explicit override.
53
+ */
54
+ export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
@@ -53,7 +53,7 @@ export function buildSchemaAST(entities, options = {}) {
53
53
  * reflection also produces constructor values like `String`/`Number`.
54
54
  * `columnType` remains the unambiguous, always-respected explicit override.
55
55
  */
56
- function resolveColumnCanonicalType(field, seen = new Set()) {
56
+ export function resolveColumnCanonicalType(field, seen = new Set()) {
57
57
  const hasExplicitType = !!field.columnType || !field.typeFromReference;
58
58
  if (!hasExplicitType && field.references && !seen.has(field.references)) {
59
59
  seen.add(field.references);
@@ -741,7 +741,10 @@ export type EntityIndexMeta = {
741
741
  } & VectorIndexOptions & IndexTypeOptions;
742
742
  export type EntityMeta<E> = {
743
743
  readonly entity: Type<E>;
744
+ /** The table, which is the class's own name where the entity named none - see {@link derivedName}. */
744
745
  name?: string;
746
+ /** Whether {@link name} came from the class rather than from the author, so a naming strategy applies. */
747
+ derivedName?: boolean;
745
748
  /** Set only when the entity named one; unset defers to the pool where it is used. See `AbstractDialect.resolveSchema`. */
746
749
  schema?: string;
747
750
  /**
@@ -794,6 +797,16 @@ export type CheckOptions = {
794
797
  readonly name?: string;
795
798
  readonly expression: QueryRaw;
796
799
  };
800
+ /**
801
+ * An entity's members as the registry takes them, keyed by plain strings - what a decorator bag, an
802
+ * {@link EntityOptions} and a decorator bag both reduce to before anything is registered: a member
803
+ * decorator has no class to key against, so by then the keys are plain strings either way.
804
+ */
805
+ export type EntityMembers = {
806
+ readonly fields?: Readonly<Record<string, FieldOptions | undefined>>;
807
+ readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
808
+ readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
809
+ };
797
810
  export type EntityOptions<E = unknown> = {
798
811
  readonly name?: string;
799
812
  /**
@@ -206,6 +206,19 @@ export interface SchemaDiff {
206
206
  readonly foreignKeysToAdd?: ForeignKeySchema[];
207
207
  readonly foreignKeysToDrop?: string[];
208
208
  }
209
+ /**
210
+ * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
211
+ * `logging` reports each statement. A plan ignores `logging`, having nothing to run.
212
+ */
213
+ export interface SyncOptions {
214
+ readonly safe?: boolean;
215
+ readonly drop?: boolean;
216
+ readonly logging?: boolean;
217
+ /** One entity instead of every registered one, for a schema that grows while the process runs. */
218
+ readonly entity?: Type<unknown>;
219
+ /** Drop every table and recreate it. Development only: it is the one option that loses data. */
220
+ readonly force?: boolean;
221
+ }
209
222
  export interface CreateSchemaOptions {
210
223
  readonly ifNotExists?: boolean;
211
224
  /**
@@ -296,10 +296,20 @@ type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStr
296
296
  type QueryAllowedOp<T> = QueryCommonOp | ([NonNullable<T>] extends [QueryComparableScalar] ? QueryOrderedOp : never) | ([NonNullable<T>] extends [string] ? QueryStringOp : never) | ([NonNullable<T>] extends [readonly number[] | Uint8Array] ? QueryVectorOp : never) | (IsMany<T> extends true ? QueryArrayOp : never);
297
297
  /**
298
298
  * Operators applicable to a field of type `T`: string operators require string fields, ordering
299
- * operators comparable fields, array operators array fields. `unknown` stays fully permissive
300
- * (untyped JSON dot-paths, erased dialect shapes).
299
+ * operators comparable fields, array operators array fields.
300
+ *
301
+ * Two shapes stay fully permissive, because neither says anything to check against: `unknown`
302
+ * (untyped JSON dot-paths, erased dialect shapes), and a field typed as every scalar at once - the
303
+ * column of a content type defined at runtime. Narrowing to what they share would leave a dynamic
304
+ * row with equality alone, since no operator applies to a boolean and a blob both.
305
+ */
306
+ export type QueryWhereFieldOperators<T> = unknown extends T ? QueryWhereFieldOperatorMap<T> : IsUntypedColumn<T> extends true ? QueryWhereFieldOperatorMap<T> : Pick<QueryWhereFieldOperatorMap<T>, QueryAllowedOp<T>>;
307
+ /**
308
+ * Whether a column admits every scalar at once, which is what an entity keyed by an index signature
309
+ * says about all of its columns. `Scalar` is the yardstick rather than a parameter: the question is
310
+ * whether `T` is at least that wide, and nothing narrower than the whole union answers it.
301
311
  */
302
- export type QueryWhereFieldOperators<T> = unknown extends T ? QueryWhereFieldOperatorMap<T> : Pick<QueryWhereFieldOperatorMap<T>, QueryAllowedOp<T>>;
312
+ type IsUntypedColumn<T> = [Scalar] extends [NonNullable<T>] ? true : false;
303
313
  /**
304
314
  * Value for a field comparison. A bare array is an implicit `$in` for scalar fields only:
305
315
  * on array-typed fields (e.g. a vector `number[]`) an array of arrays is ambiguous, so
@@ -21,9 +21,9 @@ export declare function isOperatorObject(value: unknown): value is Record<string
21
21
  export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
22
22
  export declare function getKeys<T extends object>(obj: T): (keyof T & string)[];
23
23
  /**
24
- * The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets
25
- * one, so the fallback is for a meta a decorator is still building - which is why the sites spelling
26
- * this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
24
+ * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
25
+ * the fallback is what an entity that named no table is called - which is why the sites spelling this
26
+ * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
27
27
  */
28
28
  export declare function entityName<E>(meta: EntityMeta<E>): string;
29
29
  export declare function getFieldKeys<E>(fields: {
@@ -53,9 +53,9 @@ export function getKeys(obj) {
53
53
  return obj ? Object.keys(obj) : [];
54
54
  }
55
55
  /**
56
- * The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets
57
- * one, so the fallback is for a meta a decorator is still building - which is why the sites spelling
58
- * this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
56
+ * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so
57
+ * the fallback is what an entity that named no table is called - which is why the sites spelling this
58
+ * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
59
59
  */
60
60
  export function entityName(meta) {
61
61
  return meta.name ?? meta.entity.name;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "JSON-native TypeScript ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.43.0",
6
+ "version": "0.44.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -145,7 +145,7 @@
145
145
  "express": "^5.2.1",
146
146
  "mariadb": "^3.5.4",
147
147
  "mongodb": "^7.6.0",
148
- "mysql2": "^3.24.3",
148
+ "mysql2": "^3.24.4",
149
149
  "pg": "^8.23.0",
150
150
  "pg-query-stream": "^4.17.0",
151
151
  "rxjs": "^7.8.2",