uql-orm 0.83.0 → 0.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <h3>The JSON-native TypeScript ORM</h3>
11
11
 
12
- <p align="left">UQL (Unified Query Language) queries SQL databases and MongoDB with plain, type-safe JSON, in a syntax inspired by MongoDB's.
12
+ <p align="left">UQL queries SQL databases and MongoDB with plain, type-safe JSON-syntax.
13
13
  </p>
14
14
 
15
15
  <p>
@@ -44,16 +44,127 @@ That is the whole install ([setup](https://uql-orm.dev/getting-started)). No com
44
44
 
45
45
  The compiler catches each of those, with no codegen: the entity classes are the schema. Try the editor [on the home page](https://uql-orm.dev).
46
46
 
47
+ ## How it fits together
48
+
49
+ ### 1. The entities are the schema
50
+
51
+ ```ts
52
+ // entities.ts
53
+ import { Entity, Field, Id, ManyToOne, OneToMany } from 'uql-orm';
54
+
55
+ @Entity()
56
+ export class User {
57
+ @Id({ type: Number })
58
+ id?: number;
59
+
60
+ @Field({ type: String, unique: true })
61
+ email?: string | null;
62
+
63
+ @OneToMany({ entity: () => Post, mappedBy: (post) => post.author })
64
+ posts?: Post[];
65
+ }
66
+
67
+ @Entity()
68
+ export class Post {
69
+ @Id({ type: Number })
70
+ id?: number;
71
+
72
+ @Field({ type: String })
73
+ title?: string | null;
74
+
75
+ @Field({ type: Number })
76
+ likes?: number | null;
77
+
78
+ @Field({ references: () => User })
79
+ authorId?: number | null;
80
+
81
+ @ManyToOne({ entity: () => User, references: (post) => post.authorId })
82
+ author?: User;
83
+ }
84
+ ```
85
+
86
+ ### 2. A pool, and the migrations it drives
87
+
88
+ ```ts
89
+ // uql.config.ts
90
+ import type { Config } from 'uql-orm';
91
+ import { PgQuerierPool } from 'uql-orm/postgres';
92
+ import { Post, User } from './entities.js';
93
+
94
+ export const pool = new PgQuerierPool({ connectionString: process.env.DATABASE_URL });
95
+
96
+ export default { pool, entities: [User, Post] } satisfies Config;
97
+ ```
98
+
99
+ ```sh
100
+ npx uql-migrate generate:entities initial # diffs the entities against the database into a migration you review
101
+ npx uql-migrate up # applies it
102
+ ```
103
+
104
+ ### 3. Query on the server
105
+
106
+ ```ts
107
+ import { Post } from './entities.js';
108
+ import { pool } from './uql.config.js';
109
+
110
+ const posts = await pool.findMany(Post, {
111
+ $select: { title: true },
112
+ $populate: { author: { $select: { email: true } } },
113
+ $where: { likes: { $gte: 10 } },
114
+ $sort: { likes: 'desc' },
115
+ $limit: 10,
116
+ });
117
+ // SELECT "Post"."title", "author"."id" "author.id", "author"."email" "author.email"
118
+ // FROM "Post" LEFT JOIN "User" "author" ON "author"."id" = "Post"."authorId"
119
+ // WHERE "Post"."likes" >= $1 ORDER BY "Post"."likes" DESC LIMIT 10
120
+ ```
121
+
122
+ The result is typed to what the query selected: `posts[0].author?.email` compiles, `posts[0].likes` does not.
123
+
124
+ ### 4. Serve it, and send the same query from the browser
125
+
126
+ ```ts
127
+ // server.ts: Bun, Deno, Cloudflare Workers, or any framework that takes a fetch handler
128
+ import { createFetchHandler } from 'uql-orm/http';
129
+ import { Post, User } from './entities.js';
130
+ import { pool } from './uql.config.js';
131
+
132
+ export default { fetch: createFetchHandler({ pool, include: [User, Post] }) };
133
+ ```
134
+
135
+ ```ts
136
+ // browser.ts
137
+ import { HttpQuerier } from 'uql-orm/browser';
138
+ import { Post } from './entities.js';
139
+
140
+ const api = new HttpQuerier('https://api.example.com');
141
+
142
+ const { data: posts } = await api.findMany(Post, {
143
+ $select: { title: true },
144
+ $populate: { author: { $select: { email: true } } },
145
+ $where: { likes: { $gte: 10 } },
146
+ $sort: { likes: 'desc' },
147
+ $limit: 10,
148
+ });
149
+ // GET /post?$select={"title":true}&$populate={"author":{"$select":{"email":true}}}&$where={"likes":{"$gte":10}}&$sort={"likes":"desc"}&$limit=10
150
+ ```
151
+
152
+ The query object is the same on both sides, and so is its type check. Authorization goes in the handler's [hooks](https://uql-orm.dev/http#authorization-hooks), and tenant isolation in [security filters](https://uql-orm.dev/multi-tenancy) that apply to every query, including the server's own.
153
+
154
+ ### When CRUD is not enough
155
+
156
+ - [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere a value or a field goes, and a migration can be plain SQL.
157
+ - [Computed fields](https://uql-orm.dev/entities/computed-fields) are SQL expressions that you can filter and sort on. [Triggers](https://uql-orm.dev/entities/triggers) run inside the database.
158
+ - [Transactions](https://uql-orm.dev/querying/transactions) hold one connection across many operations, and [lifecycle hooks](https://uql-orm.dev/entities/lifecycle-hooks) run your code around each write.
159
+ - Anything the CRUD routes do not cover goes in a route you write, beside the handler and under the same prefix.
160
+
47
161
  ## Why UQL?
48
162
 
49
- - **Queries are JSON, not method chains.** Build one dynamically, store it, or send it from the browser; the same object runs on every database. No DSL to learn.
50
163
  - **One API, everywhere it runs.** PostgreSQL, PGlite, CockroachDB, MySQL, MariaDB, MSSQL, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun's native SQL, and even MongoDB. The same code on Node 24+, Bun, Deno, [Cloudflare Workers](https://uql-orm.dev/cloudflare-d1), [AWS Lambda and Vercel](https://uql-orm.dev/serverless), and [the browser](https://uql-orm.dev/browser), with no native binaries on the `fetch`-based drivers.
51
164
  - **Type-safe to the leaf, nothing to generate.** Every key is checked against your entity, down into populated relations and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths, so `$like` on a numeric column is a compile error. No `.prisma` file, no generated client.
52
165
  - **Relations without N+1.** [`$populate`](https://uql-orm.dev/querying/relations) reads a to-many inside the parent's statement, so a read is one round trip. Nothing is lazy, so nothing fires behind your back in a serializer.
53
- - **Migrations you read before they run.** Edit an entity, run `uql-migrate generate:entities`, review the SQL in the PR like any other file. [`drift:check`](https://uql-orm.dev/migrations) catches a database that no longer matches.
54
- - **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [computed fields](https://uql-orm.dev/entities/computed-fields) are expressions you can filter on, and a migration can be plain SQL.
55
166
  - **Light.** Zero runtime dependencies and every dialect in one package, yet `uql-orm/postgres` is about 27 kB gzipped. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
56
- - **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
167
+ - **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [drift checks](https://uql-orm.dev/migrations) that catch a database that no longer matches.
57
168
  - **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), on Bun, Node and Deno alike. The same benchmark [scores the types](https://github.com/rogerpadilla/ts-orm-benchmark#type-safety) by compiling ordinary mistakes in each ORM's API: UQL is the only one that catches them all.
58
169
 
59
170
  ## Get started
@@ -1,4 +1,4 @@
1
- var c=[];function d(e){for(let r of c)r(e)}function A(e){c.push(e);let r=c.length-1;return()=>{c.splice(r,1)}}var O=["$select","$populate","$exclude","$where","$sort"],f=["$count"],T=["$skip","$limit"],R=["$candidates"],b=["$distinct"],v=["$lock",...f,...R];var C=Symbol("rawValue"),W=Symbol("rawAlias"),B=Symbol("rawText");class i extends TypeError{name="UqlUsageError";kind="usage";status=400}function l(e){return e?Object.keys(e):[]}function k(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var z=new Set([...O,...f,...T,...R,...b,"hardDelete","count"]);function u(e){if(!e)return"";let r=new URLSearchParams;for(let n of l(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?a(o):String(o))}let t=r.toString();return t?`?${t}`:""}function a(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(C in t)throw new i("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw new i("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function h(e,r){return y(e,{method:"get"},r)}function x(e,r,t){return y(e,{method:"post",body:a(r)},t)}function Q(e,r,t){return y(e,{method:"patch",body:a(r)},t)}function g(e,r,t){return y(e,{method:"put",body:a(r)},t)}function m(e,r){return y(e,{method:"delete"},r)}function U(e,r,t){return y(e,{method:"QUERY",body:a(r)},t)}function y(e,r,t){if(d({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return d({phase:"success",opts:t}),o;let P=o,E={message:P?.error?.message??n.statusText,code:P?.error?.code??n.status};throw d({phase:"error",error:E,opts:t}),new K(E.message,E.code)})).finally(()=>{d({phase:"complete",opts:t})})}function q(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},F=l(s),ie=new Map(F.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function V(e){return q(e.name)}function w(e,r){if(!k(r))throw new i(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class S{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),p=u(t);return h(`${o}/${w(e,r)}${p}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return x(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return x(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return Q(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),p=u(r);return Q(`${o}${p}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return g(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return g(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?u({hardDelete:t.hardDelete}):"";return m(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=u(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return m(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??V)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return U(e,r??{},this.buildOptions(t));return h(`${e}${u(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var j={getQuerier:()=>new S("/api")};function fe(e){j=e}function _(){return j}function Re(){return _().getQuerier()}export{S as HttpQuerier,K as RequestError,h as get,Re as getQuerier,_ as getQuerierPool,d as notify,A as on,Q as patch,x as post,g as put,U as query,m as remove,fe as setQuerierPool};
1
+ var c=[];function d(e){for(let r of c)r(e)}function A(e){c.push(e);let r=c.length-1;return()=>{c.splice(r,1)}}var O=["$select","$populate","$exclude","$where","$sort"],f=["$count"],b=["$skip","$limit"],R=["$candidates"],T=["$distinct"],v=["$lock",...f,...R];var C=Symbol("rawValue"),W=Symbol("rawAlias"),B=Symbol("rawText");class i extends TypeError{name="UqlUsageError";kind="usage";status=400}function l(e){return e?Object.keys(e):[]}function k(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Z=new Set([...O,...f,...b,...R,...T,"hardDelete","count"]);function u(e){if(!e)return"";let r=new URLSearchParams;for(let n of l(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?a(o):String(o))}let t=r.toString();return t?`?${t}`:""}function a(e){return JSON.stringify(e,(r,t)=>{if(typeof t!=="object"||t===null)return t;if(C in t)throw new i("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw new i("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class K extends Error{status;constructor(e,r){super(e);this.status=r;this.name="RequestError"}}function h(e,r){return y(e,{method:"get"},r)}function x(e,r,t){return y(e,{method:"post",body:a(r)},t)}function Q(e,r,t){return y(e,{method:"patch",body:a(r)},t)}function g(e,r,t){return y(e,{method:"put",body:a(r)},t)}function m(e,r){return y(e,{method:"delete"},r)}function U(e,r,t){return y(e,{method:"QUERY",body:a(r)},t)}function y(e,r,t){if(d({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return d({phase:"success",opts:t}),o;let P=o,E={message:P?.error?.message??n.statusText,code:P?.error?.code??n.status};throw d({phase:"error",error:E,opts:t}),new K(E.message,E.code)})).finally(()=>{d({phase:"complete",opts:t})})}function q(e){let r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},F=l(s),ue=new Map(F.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return q(e.name)}function w(e,r){if(!k(r))throw new i(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class S{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),p=u(t);return h(`${o}/${w(e,r)}${p}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return x(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return x(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return Q(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),p=u(r);return Q(`${o}${p}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return g(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return g(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?u({hardDelete:t.hardDelete}):"";return m(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=u(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return m(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return U(e,r??{},this.buildOptions(t));return h(`${e}${u(r)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var V={getQuerier:()=>new S("/api")};function Re(e){V=e}function _(){return V}function he(){return _().getQuerier()}export{S as HttpQuerier,K as RequestError,h as get,he as getQuerier,_ as getQuerierPool,d as notify,A as on,Q as patch,x as post,g as put,U as query,m as remove,Re as setQuerierPool};
2
2
 
3
- //# debugId=0F47F63ADFD03A2764756E2164756E21
3
+ //# debugId=54F53C9583121D9864756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -7,14 +7,14 @@
7
7
  "import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';\nimport type { Type } from './utility.js';\n\n/** What a `raw` callback receives. See {@link QueryRawFn}. */\nexport type QueryRawRenderOptions = {\n /** The dialect rendering the SQL. */\n dialect: SqlQueryDialect;\n /** The alias of the table in scope, unescaped; empty where there is none. */\n prefix: string;\n /** {@link prefix} escaped, with its trailing dot. */\n escapedPrefix: string;\n /** The query context the SQL is written into. */\n ctx: QueryContext;\n /**\n * The entity being rendered, which a ref read off a definition's map resolves its column against: a\n * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.\n */\n entity?: Type<unknown>;\n /**\n * The `FROM` a set-based trigger's body reads its rows through, `FROM inserted` and the like, which a\n * write in it names. Absent where the body reads `NEW` and `OLD` bare, and outside a trigger.\n */\n rows?: string;\n};\n\n/** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */\nexport type QueryRawFnOptions = Partial<QueryRawRenderOptions>;\n\n/**\n * A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else\n * it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the\n * latter rejected `({ ctx }) => ctx.append(...)`, the form every computed field is written in, because\n * TypeScript's \"returning a value where void is expected\" allowance does not apply to a union.\n */\nexport type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;\n\nexport const RAW_VALUE: unique symbol = Symbol('rawValue');\nexport const RAW_ALIAS: unique symbol = Symbol('rawAlias');\nexport const RAW_TEXT: unique symbol = Symbol('rawText');\n\nexport class QueryRaw {\n readonly [RAW_VALUE]: QueryRawFn;\n readonly [RAW_ALIAS]?: string;\n /**\n * The SQL verbatim, set only where it is a constant: a template that interpolates nothing binds no\n * value and reads no column, so it needs no dialect to render. What a DDL clause with nowhere to\n * bind reads - see {@link constantSql}.\n */\n readonly [RAW_TEXT]?: string;\n\n constructor(value: QueryRawFn, alias?: string, text?: string) {\n this[RAW_VALUE] = value;\n this[RAW_ALIAS] = alias;\n this[RAW_TEXT] = text;\n }\n\n /** The same expression under an alias, for a `$select` projection. */\n as(alias: string): QueryRaw {\n return new QueryRaw(this[RAW_VALUE], alias, this[RAW_TEXT]);\n }\n\n /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */\n render(opts: QueryRawRenderOptions): void {\n const emitted = this[RAW_VALUE](opts);\n if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {\n opts.ctx.append(String(emitted));\n }\n }\n}\n\n/**\n * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it\n * renders as the field's column. Its `key` is how an index tells a column from an expression, and `V`,\n * the field's type, is what a value slot checks it against: see {@link RawFor}.\n */\nexport class ColumnRef<K extends string = string, V = unknown> extends QueryRaw {\n declare readonly __value?: V;\n\n constructor(\n readonly key: K,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n\n/**\n * SQL where a value of type `V` goes: bare SQL, whose type is its author's to know, or a ref to a column\n * holding one, nullability aside. `Raw` is what the transport carries, so the wire's `never` stays one.\n */\nexport type RawFor<Raw, V> = Raw & { readonly __value?: V | null };\n\n/**\n * A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.\n * It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and\n * sorted like any other.\n *\n * `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom\n * fields so the aggregate a field declares decides the property's type and refuses `stored: true` on\n * one no delta can maintain.\n */\nexport class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {\n declare readonly __value?: V;\n declare private readonly __storable: Storable;\n\n constructor(\n /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */\n readonly spec: RelationAggregateSpec,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n",
8
8
  "/**\n * What a failed query ran into, named the same on every engine - what {@link queryErrorKind} answers\n * with, whether a driver raised the error or UQL did. `retryable` is a deadlock, a serialization\n * failure, a lock timeout or a busy database: the transaction can simply run again. `usage` is the\n * caller's own mistake, which running it again will not fix.\n */\nexport type QueryErrorKind =\n | 'uniqueViolation'\n | 'foreignKeyViolation'\n | 'notNullViolation'\n | 'checkViolation'\n | 'optimisticLock'\n | 'retryable'\n | 'usage';\n\n/**\n * Thrown where the caller used the API in a way no statement can carry out: an update payload with no\n * version, a `$lock` outside a transaction, a method with no version to match. A `TypeError` still,\n * since the call itself is wrong, but one carrying the `status` an HTTP transport answers with - the\n * request is malformed, not the server's failure, and an untyped client is exactly who reaches this.\n */\nexport class UqlUsageError extends TypeError {\n override name = 'UqlUsageError';\n /** What `queryErrorKind` answers, so a caller branches on it rather than on the class. */\n readonly kind = 'usage';\n /** What an HTTP transport answers with. */\n readonly status = 400;\n}\n\n/** What a value is, for a refusal naming what `/http` handed over instead of what the types require. */\nexport function kindOf(value: unknown): string {\n return value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;\n}\n\n/**\n * @deprecated since 0.77.1 - use {@link UqlUsageError}, which every misuse throws, lock or not. The\n * same class under both names, so an existing `instanceof` keeps working.\n */\nexport const UqlLockUsageError = UqlUsageError;\nexport type UqlLockUsageError = UqlUsageError;\n\n/**\n * Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,\n * or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`\n * where there is no row left.\n */\nexport class UqlOptimisticLockError extends Error {\n override name = 'UqlOptimisticLockError';\n readonly kind = 'optimisticLock';\n readonly status = 409;\n\n constructor(\n message: string,\n readonly expected: unknown,\n readonly actual: unknown,\n ) {\n super(message);\n }\n}\n",
9
9
  "import type { EntityMeta } from '../type/index.js';\nimport { UqlUsageError } from './uqlError.js';\n\nexport function throwPendingTransaction(): never {\n throw new UqlUsageError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw new UqlUsageError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether `key` names an operator (`$eq`, `$push`...) rather than a field. */\nexport function isOperatorKey(key: string): boolean {\n return key.startsWith('$');\n}\n\n/** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether `value` is an object that is not an array, whose keys can be read. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nexport function getKeys<T extends object>(obj: T | null | undefined): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\n/** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */\nexport function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][] {\n return (Object.entries(record) as [K, V | undefined][]).filter((entry): entry is [K, V] => entry[1] !== undefined);\n}\n\n/**\n * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so\n * the fallback is what an entity that named no table is called - which is why the sites spelling this\n * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\n/**\n * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the\n * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which\n * is what a `$where` map and a composite key's id object both are; an array is a list of either.\n */\nexport function isScalarId(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) {\n return true;\n }\n if (Array.isArray(value)) {\n return false;\n }\n // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser\n // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column\n // with a map of several.\n const proto = Object.getPrototypeOf(value);\n return proto !== Object.prototype && proto !== null;\n}\n\n/** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */\nexport function isWhereMap(value: unknown): value is Record<string, unknown> {\n return !Array.isArray(value) && !isScalarId(value);\n}\n",
10
- "import type { QueryOptions, WireQuery } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.js';\n// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`\n// with it, in the browser bundle, which is on a size budget\nimport { RAW_VALUE } from '../type/queryRaw.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys, isWhereMap } from '../util/object.util.js';\n// the error class alone, from its own leaf module: `queryError.ts` carries every driver's code map\nimport { UqlUsageError } from '../util/uqlError.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof WireQuery<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof WireQuery<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams<E = unknown>(params: Record<string, unknown> = {}): WireQuery<E> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw new UqlUsageError(`'${key}' is not supported over HTTP`);\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n if (!isWhereMap(query['$where'])) {\n throw new UqlUsageError(\"'$where' must be a JSON object\");\n }\n\n // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\n }\n\n return query;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? wireJson(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server\n * build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not\n * have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it\n * serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,\n * hits where the client's types already refuse a fragment.\n */\nexport function wireJson(value: unknown): string {\n return JSON.stringify(value, (_key: string, held: unknown) => {\n if (typeof held !== 'object' || held === null) {\n return held;\n }\n if (RAW_VALUE in held) {\n throw new UqlUsageError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');\n }\n // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.\n if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {\n throw new UqlUsageError('binary cannot travel over HTTP: what leaves the browser is JSON');\n }\n return held;\n });\n}\n",
10
+ "import type { QueryOptions, WireQuery } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.js';\n// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`\n// with it, in the browser bundle, which is on a size budget\nimport { RAW_VALUE } from '../type/queryRaw.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys, isRecord, isWhereMap } from '../util/object.util.js';\n// the error class alone, from its own leaf module: `queryError.ts` carries every driver's code map\nimport { UqlUsageError } from '../util/uqlError.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof WireQuery<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof WireQuery<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values), or a `QUERY` body, into a UQL query\n * object. Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams<E = unknown>(params: unknown = {}): WireQuery<E> {\n if (!isRecord(params)) {\n throw new UqlUsageError('the query must be a JSON object');\n }\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw new UqlUsageError(`'${key}' is not supported over HTTP`);\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n if (!isWhereMap(query['$where'])) {\n throw new UqlUsageError(\"'$where' must be a JSON object\");\n }\n\n // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\n }\n\n return query;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? wireJson(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server\n * build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not\n * have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it\n * serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,\n * hits where the client's types already refuse a fragment.\n */\nexport function wireJson(value: unknown): string {\n return JSON.stringify(value, (_key: string, held: unknown) => {\n if (typeof held !== 'object' || held === null) {\n return held;\n }\n if (RAW_VALUE in held) {\n throw new UqlUsageError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');\n }\n // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.\n if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {\n throw new UqlUsageError('binary cannot travel over HTTP: what leaves the browser is JSON');\n }\n return held;\n });\n}\n",
11
11
  "import type { RequestErrorResponse } from '../../http/contract.js';\nimport { wireJson } from '../../http/query.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 return request<T>(url, { method: 'post', body: wireJson(payload) }, opts);\n}\n\nexport function patch<T>(url: string, payload: unknown, opts?: RequestOptions) {\n return request<T>(url, { method: 'patch', body: wireJson(payload) }, opts);\n}\n\nexport function put<T>(url: string, payload: unknown, opts?: RequestOptions) {\n return request<T>(url, { method: 'put', body: wireJson(payload) }, 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 return request<T>(url, { method: 'QUERY', body: wireJson(payload) }, 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",
12
12
  "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 return text.charAt(0).toUpperCase() + text.slice(1);\n}\n\nexport function lowerFirst(text: string): string {\n return text.charAt(0).toLowerCase() + text.slice(1);\n}\n\nexport function snakeCase(val: string): 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",
13
13
  "import { queryErrorKind } from '../querier/queryError.js';\nimport 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';\nimport type { QueryErrorKind } from '../util/uqlError.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/** Generic on purpose: a driver's constraint message names tables and constraints, and Postgres echoes the value. */\nconst CONSTRAINT_ERRORS: ReadonlyMap<QueryErrorKind | undefined, RequestErrorResponse['error']> = new Map([\n ['uniqueViolation', { message: 'Conflict', code: 409 }],\n ['foreignKeyViolation', { message: 'Conflict', code: 409 }],\n ['notNullViolation', { message: 'Bad Request', code: 400 }],\n ['checkViolation', { message: 'Bad Request', code: 400 }],\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), then a constraint violation (409/400), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const error =\n err instanceof Error && 'status' in err && typeof err.status === 'number'\n ? { message: err.message, code: err.status }\n : (CONSTRAINT_ERRORS.get(queryErrorKind(err)) ?? {\n message: err instanceof Error ? err.message : 'Internal Server Error',\n code: 500,\n });\n return { status: error.code, body: { error } };\n}\n",
14
14
  "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityWrite,\n EntityId,\n FieldKey,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdateWrite,\n WireQuery,\n WrittenId,\n} from '../../type/index.js';\nimport { isScalarId } from '../../util/object.util.js';\nimport { UqlUsageError } from '../../util/uqlError.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n /**\n * The URL segment an entity is addressed by, defaulting to its kebab-cased class name - the same\n * option the server handler takes, so one map serves both. State it where the default cannot: a\n * build that minifies class names renames every route.\n */\n readonly entityPath?: (entity: Type<unknown>) => string;\n};\n\n/** The id as one path segment, refusing a composite key, which has no spelling in `/:id` yet. Callers are `async`. */\nfunction idSegment<E>(entity: Type<E>, id: EntityId<E>): string {\n if (!isScalarId(id)) {\n throw new UqlUsageError(`'${entity.name}' was addressed by an id object, which the HTTP route cannot carry.`);\n }\n return String(id);\n}\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n async findOneById<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n id: EntityId<E>,\n q?: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${basePath}/${idSegment(entity, id)}${qs}`,\n this.buildOptions(opts),\n );\n }\n\n findOne<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n return this.read<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`,\n q,\n opts,\n );\n }\n\n findMany<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: WireQuery<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<QueryFindResult<E, S, V, X, P, C>[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QueryPage<E, never>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n /** The `count` route capped at one row, so existence needs no endpoint of its own. */\n async exists<E extends object>(entity: Type<E>, q?: QueryFilter<E, never>, opts?: RequestOptions) {\n const res = await this.count(entity, { ...q, $limit: 1 }, opts);\n return { ...res, data: res.data > 0 };\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.insertMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async updateOneById<E extends object>(\n entity: Type<E>,\n id: EntityId<E>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${idSegment(entity, id)}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(\n entity: Type<E>,\n q: QuerySearch<E, never>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: readonly EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.saveMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${idSegment(entity, id)}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E, never>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${(this.defaults.entityPath ?? entityPath)(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
15
15
  "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"
16
16
  ],
17
- "mappings": "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",
18
- "debugId": "0F47F63ADFD03A2764756E2164756E21",
17
+ "mappings": "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",
18
+ "debugId": "54F53C9583121D9864756E2164756E21",
19
19
  "names": []
20
20
  }
@@ -708,7 +708,6 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
708
708
  * throw, the way {@link appendTextSearch} already does.
709
709
  */
710
710
  protected regexCondition(operand: string, placeholder: string): string;
711
- protected get likeFn(): string;
712
711
  /**
713
712
  * Two fragments compared null-safely: true where they differ, and where one side alone is NULL. What a
714
713
  * trigger compares a column's two rows with, where the portable `<>` would miss a column set to or
@@ -1,7 +1,7 @@
1
1
  import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
2
2
  import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, } from '../type/index.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
- import { asSelectMap, assertNonNegativeInteger, assertWhere, definedEntries, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { isSelectList, assertNonNegativeInteger, assertWhere, definedEntries, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
6
6
  import { kindOf, UqlUsageError } from '../util/uqlError.js';
7
7
  import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
@@ -22,11 +22,18 @@ export function relationTermKey({ sql, key }) {
22
22
  * none is left of a row crossing JSON, which answers only under keys.
23
23
  */
24
24
  function projectedKeys(meta, select, exclude, json) {
25
- const selected = Array.isArray(select)
26
- ? select
27
- : normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
25
+ const selected = isSelectList(select)
26
+ ? select.map(selectRaw)
27
+ : normalizeScalarFieldSelection(meta, select, exclude);
28
28
  return selected.length || !json ? selected : normalizeScalarFieldSelection(meta);
29
29
  }
30
+ /** An item of a `$select` list, which only `raw()` fills: anything else arrived from an untyped client. */
31
+ function selectRaw(item) {
32
+ if (item instanceof QueryRaw) {
33
+ return item;
34
+ }
35
+ throw new UqlUsageError(`a $select list takes raw() expressions only, not a ${kindOf(item)}: name fields in its map form, { field: true }`);
36
+ }
30
37
  /** `value`, where there is one; a `UqlUsageError` saying `refusal` where there is none. */
31
38
  function orRefuse(value, refusal) {
32
39
  if (value === undefined) {
@@ -564,8 +571,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
564
571
  const fold = like.insensitive && this.caseInsensitiveMatch === 'fold';
565
572
  const value = String(val);
566
573
  const ph = this.addValue(ctx, like.pattern(fold ? value.toLowerCase() : value));
567
- const matchOp = like.insensitive && this.caseInsensitiveMatch === 'ilike' ? 'ILIKE' : this.likeFn;
568
- return `${fold ? `LOWER(${operand})` : operand} ${matchOp} ${ph}`;
574
+ const matchOp = like.insensitive && this.caseInsensitiveMatch === 'ilike' ? 'ILIKE' : 'LIKE';
575
+ // Stated on every engine, although only SQLite and SQL Server lack `\` as their default escape.
576
+ return `${fold ? `LOWER(${operand})` : operand} ${matchOp} ${ph} ESCAPE ${this.escape('\\')}`;
569
577
  }
570
578
  /** Builds `prefix.column` from an already-resolved field, through the same memo writes use. */
571
579
  columnWithPrefix(key, field, opts) {
@@ -1969,9 +1977,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1969
1977
  regexCondition(operand, placeholder) {
1970
1978
  return `${operand} ${this.regexpOp} ${placeholder}`;
1971
1979
  }
1972
- get likeFn() {
1973
- return 'LIKE';
1974
- }
1975
1980
  /** `operand IN (...)` of each value as `bind` renders it, or the constant an empty set reduces to: no value is in it. */
1976
1981
  formatIn(_ctx, operand, values, negate, bind) {
1977
1982
  if (!values.length) {
@@ -61,6 +61,22 @@ export declare const ORDERED_OPS: ReadonlySet<string>;
61
61
  export declare function isOrderedOp(op: string): op is QueryOrderedOp;
62
62
  /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
63
63
  export declare const EQUALITY_OPS: ReadonlySet<string>;
64
+ /**
65
+ * `value` as a `$like` pattern matching it literally. The pattern language is the same on every engine:
66
+ * `%` and `_` are wildcards, `\` escapes, and `[` is escaped since SQL Server reads it as a character class.
67
+ */
68
+ export declare function likeLiteral(value: string): string;
69
+ /**
70
+ * `pattern` with its `[` escaped, as {@link likeLiteral} does; refused where its last `\` has nothing
71
+ * after it to escape (`'John\'`): Postgres throws on one, MySQL reads it literally and SQLite matches nothing.
72
+ */
73
+ export declare function likePattern(pattern: string): string;
74
+ /**
75
+ * A `$like` pattern as the anchored regex matching the same strings, for an engine with no `LIKE`. A
76
+ * wildcard spans lines, as it does in SQL; an edge `%` drops its anchor, which keeps a prefix indexable.
77
+ * The end anchor is `\z`, since `$` also matches before a trailing newline.
78
+ */
79
+ export declare function likeRegex(pattern: string): string;
64
80
  /**
65
81
  * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
66
82
  * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
@@ -122,6 +122,49 @@ export function isOrderedOp(op) {
122
122
  }
123
123
  /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
124
124
  export const EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
125
+ /**
126
+ * `value` as a `$like` pattern matching it literally. The pattern language is the same on every engine:
127
+ * `%` and `_` are wildcards, `\` escapes, and `[` is escaped since SQL Server reads it as a character class.
128
+ */
129
+ export function likeLiteral(value) {
130
+ return value.replace(/[\\%_[]/g, '\\$&');
131
+ }
132
+ /**
133
+ * `pattern` with its `[` escaped, as {@link likeLiteral} does; refused where its last `\` has nothing
134
+ * after it to escape (`'John\'`): Postgres throws on one, MySQL reads it literally and SQLite matches nothing.
135
+ */
136
+ export function likePattern(pattern) {
137
+ const tokens = likeTokens(pattern);
138
+ const last = tokens.at(-1);
139
+ if (last?.char === '\\' && !last.escaped) {
140
+ throw new UqlUsageError("a $like pattern cannot end in a '\\' with nothing after it to escape: write '\\\\' to match a backslash");
141
+ }
142
+ return tokens.map(({ char, escaped }) => (escaped || char === '[' ? `\\${char}` : char)).join('');
143
+ }
144
+ /**
145
+ * A `$like` pattern as the anchored regex matching the same strings, for an engine with no `LIKE`. A
146
+ * wildcard spans lines, as it does in SQL; an edge `%` drops its anchor, which keeps a prefix indexable.
147
+ * The end anchor is `\z`, since `$` also matches before a trailing newline.
148
+ */
149
+ export function likeRegex(pattern) {
150
+ const tokens = likeTokens(pattern).map(({ char, escaped }) => (escaped ? undefined : LIKE_WILDCARDS[char]) ?? escapeRegex(char));
151
+ const open = tokens[0] === LIKE_WILDCARDS['%'];
152
+ const closed = tokens.at(-1) !== LIKE_WILDCARDS['%'];
153
+ const body = tokens.slice(open ? 1 : 0, closed ? undefined : -1).join('');
154
+ return `${open ? '' : '^'}${body}${closed ? String.raw `\z` : ''}`;
155
+ }
156
+ /** A `$like` pattern's characters, each marked where a `\` escaped it; a last `\` escaping nothing reads as plain. */
157
+ function likeTokens(pattern) {
158
+ return [...pattern.matchAll(/\\([\s\S])|[\s\S]/g)].map(([token, escaped]) => ({
159
+ char: escaped ?? token,
160
+ escaped: escaped !== undefined,
161
+ }));
162
+ }
163
+ /** Each `$like` wildcard as the regex matching it, across lines as SQL does. */
164
+ const LIKE_WILDCARDS = { '%': String.raw `[\s\S]*`, _: String.raw `[\s\S]` };
165
+ function escapeRegex(char) {
166
+ return char.replace(/[\\^$.*+?()[\]{}|]/, '\\$&');
167
+ }
125
168
  /**
126
169
  * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
127
170
  * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
@@ -129,10 +172,10 @@ export const EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
129
172
  * `AbstractSqlDialect.caseInsensitiveMatch`'s single call.
130
173
  */
131
174
  export const LIKE_OPS = new Map([
132
- ['$like', '$ilike', (v) => v],
133
- ['$startsWith', '$istartsWith', (v) => `${v}%`],
134
- ['$endsWith', '$iendsWith', (v) => `%${v}`],
135
- ['$includes', '$iincludes', (v) => `%${v}%`],
175
+ ['$like', '$ilike', likePattern],
176
+ ['$startsWith', '$istartsWith', (v) => `${likeLiteral(v)}%`],
177
+ ['$endsWith', '$iendsWith', (v) => `%${likeLiteral(v)}`],
178
+ ['$includes', '$iincludes', (v) => `%${likeLiteral(v)}%`],
136
179
  ].flatMap(([sensitive, insensitive, pattern]) => [
137
180
  [sensitive, { pattern, insensitive: false }],
138
181
  [insensitive, { pattern, insensitive: true }],
@@ -42,13 +42,12 @@ export function createRequestHandler(opts) {
42
42
  };
43
43
  async function run(entity, { op, method, id }, req) {
44
44
  const meta = getMeta(entity);
45
- // QUERY (RFC 10008) carries the JSON query in the body instead of the query string
46
- const rawQuery = method === 'QUERY' ? req.body : req.query;
47
45
  const hookCtx = {
48
46
  meta,
49
47
  op,
50
48
  method,
51
- query: parseQueryParams(rawQuery),
49
+ // QUERY (RFC 10008) carries the JSON query in the body instead of the query string
50
+ query: parseQueryParams(method === 'QUERY' ? req.body : req.query),
52
51
  body: req.body,
53
52
  context: req.context,
54
53
  };
@@ -69,11 +68,9 @@ export function createRequestHandler(opts) {
69
68
  else {
70
69
  await preFilter?.(hookCtx);
71
70
  }
72
- const resp = await dispatch();
73
- if (post) {
74
- await post(hookCtx, resp.body);
75
- }
76
- return resp;
71
+ const envelope = await dispatch();
72
+ await post?.(hookCtx, envelope);
73
+ return { status: 200, body: envelope };
77
74
  });
78
75
  function dispatch() {
79
76
  // read post-hooks so both in-place mutation and reassignment of hookCtx.query apply
@@ -84,59 +81,59 @@ export function createRequestHandler(opts) {
84
81
  case 'findOne':
85
82
  return withQuerier(async (querier) => {
86
83
  const data = await querier.findOne(entity, query);
87
- return ok({ data, count: data ? 1 : 0 });
84
+ return { data, count: data ? 1 : 0 };
88
85
  });
89
86
  case 'count':
90
87
  return withQuerier(async (querier) => {
91
88
  const count = await querier.count(entity, query);
92
- return ok({ data: count, count });
89
+ return { data: count, count };
93
90
  });
94
91
  case 'findOneById':
95
92
  return withQuerier(async (querier) => {
96
93
  const data = await querier.findOne(entity, buildIdQuery(meta, id, query));
97
- return ok({ data, count: data ? 1 : 0 });
94
+ return { data, count: data ? 1 : 0 };
98
95
  });
99
96
  case 'findMany':
100
97
  return withQuerier(async (querier) => {
101
98
  const findManyPromise = querier.findMany(entity, query);
102
99
  const countPromise = flags.count ? querier.count(entity, query) : undefined;
103
100
  const [data, count] = await Promise.all([findManyPromise, countPromise]);
104
- return ok({ data, count });
101
+ return { data, count };
105
102
  });
106
103
  case 'insertOne':
107
104
  return withTransaction(async (querier) => {
108
105
  const data = await querier.insertOne(entity, hookCtx.body);
109
- return ok({ data, count: 1 });
106
+ return { data, count: 1 };
110
107
  });
111
108
  case 'insertMany':
112
109
  return withTransaction(async (querier) => {
113
110
  const data = await querier.insertMany(entity, hookCtx.body);
114
- return ok({ data, count: data.length });
111
+ return { data, count: data.length };
115
112
  });
116
113
  case 'saveOne':
117
114
  return withTransaction(async (querier) => {
118
115
  const data = await querier.saveOne(entity, hookCtx.body);
119
- return ok({ data, count: 1 });
116
+ return { data, count: 1 };
120
117
  });
121
118
  case 'saveMany':
122
119
  return withTransaction(async (querier) => {
123
120
  const data = await querier.saveMany(entity, hookCtx.body);
124
- return ok({ data, count: data.length });
121
+ return { data, count: data.length };
125
122
  });
126
123
  case 'updateOneById':
127
124
  return withTransaction(async (querier) => {
128
125
  const count = await querier.updateMany(entity, buildIdQuery(meta, id, query), hookCtx.body);
129
- return ok({ data: id, count });
126
+ return { data: id, count };
130
127
  });
131
128
  case 'updateMany':
132
129
  return withTransaction(async (querier) => {
133
130
  const count = await querier.updateMany(entity, query, hookCtx.body);
134
- return ok({ data: count, count });
131
+ return { data: count, count };
135
132
  });
136
133
  case 'deleteOneById':
137
134
  return withTransaction(async (querier) => {
138
135
  const count = await querier.deleteMany(entity, buildIdQuery(meta, id, query), { hardDelete });
139
- return ok({ data: id, count });
136
+ return { data: id, count };
140
137
  });
141
138
  case 'deleteMany':
142
139
  return withTransaction(async (querier) => {
@@ -148,15 +145,12 @@ export function createRequestHandler(opts) {
148
145
  ids = founds.map((found) => found[idKey]);
149
146
  count = await querier.deleteMany(entity, { $where: whereIds(meta, ids) }, { hardDelete });
150
147
  }
151
- return ok({ data: ids, count });
148
+ return { data: ids, count };
152
149
  });
153
150
  }
154
151
  }
155
152
  }
156
153
  }
157
- function ok(body) {
158
- return { status: 200, body };
159
- }
160
154
  function buildIdQuery(meta, id, query) {
161
155
  query.$where = whereWith(soleIdOf(meta, 'the HTTP handler'), id, query.$where);
162
156
  return query;
@@ -1,9 +1,9 @@
1
1
  import type { WireQuery } from '../type/index.js';
2
2
  /**
3
- * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
4
- * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
3
+ * Parse raw query-string entries (with JSON-stringified values), or a `QUERY` body, into a UQL query
4
+ * object. Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
5
5
  */
6
- export declare function parseQueryParams<E = unknown>(params?: Record<string, unknown>): WireQuery<E>;
6
+ export declare function parseQueryParams<E = unknown>(params?: unknown): WireQuery<E>;
7
7
  /**
8
8
  * Serialize a UQL query object into a percent-encoded query string where object values
9
9
  * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
@@ -4,7 +4,7 @@ import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUER
4
4
  // with it, in the browser bundle, which is on a size budget
5
5
  import { RAW_VALUE } from '../type/queryRaw.js';
6
6
  // the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
7
- import { getKeys, isWhereMap } from '../util/object.util.js';
7
+ import { getKeys, isRecord, isWhereMap } from '../util/object.util.js';
8
8
  // the error class alone, from its own leaf module: `queryError.ts` carries every driver's code map
9
9
  import { UqlUsageError } from '../util/uqlError.js';
10
10
  /**
@@ -30,10 +30,13 @@ const ALLOWED_QUERY_KEYS = new Set([
30
30
  */
31
31
  const REJECTED_QUERY_KEYS = new Set(['$lock']);
32
32
  /**
33
- * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
34
- * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
33
+ * Parse raw query-string entries (with JSON-stringified values), or a `QUERY` body, into a UQL query
34
+ * object. Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
35
35
  */
36
36
  export function parseQueryParams(params = {}) {
37
+ if (!isRecord(params)) {
38
+ throw new UqlUsageError('the query must be a JSON object');
39
+ }
37
40
  const query = {};
38
41
  for (const key of getKeys(params)) {
39
42
  if (REJECTED_QUERY_KEYS.has(key)) {
@@ -1,12 +1,12 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
3
  import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
- import { betweenBounds, GROUP_OPS, groupClauses, isGroupOp, whereOperators } from '../dialect/operators.js';
4
+ import { betweenBounds, GROUP_OPS, groupClauses, isGroupOp, LIKE_OPS, likeRegex, whereOperators, } from '../dialect/operators.js';
5
5
  import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
6
6
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
7
7
  import { COUNT_RESULT_KEY } from '../type/query.js';
8
8
  import { QueryRaw } from '../type/queryRaw.js';
9
- import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
9
+ import { aggregateOf, isSelectList, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
10
10
  import { UqlUsageError } from '../util/uqlError.js';
11
11
  import { decodeBigIntsExcept } from '../util/wideNumber.js';
12
12
  import { textLanguage } from './textLanguage.js';
@@ -71,17 +71,6 @@ function compareCount(count, size) {
71
71
  }
72
72
  return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
73
73
  }
74
- /** String operators -> { pattern: (v) => regex, caseInsensitive } */
75
- const REGEX_OP_MAP = new Map([
76
- ['$startsWith', { wrap: (v) => `^${v}`, ci: false }],
77
- ['$istartsWith', { wrap: (v) => `^${v}`, ci: true }],
78
- ['$endsWith', { wrap: (v) => `${v}$`, ci: false }],
79
- ['$iendsWith', { wrap: (v) => `${v}$`, ci: true }],
80
- ['$includes', { wrap: (v) => String(v), ci: false }],
81
- ['$iincludes', { wrap: (v) => String(v), ci: true }],
82
- ['$like', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: false }],
83
- ['$ilike', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: true }],
84
- ]);
85
74
  /** MongoDB native operators - pass through as-is. */
86
75
  const NATIVE_OPS = new Set([
87
76
  '$all',
@@ -361,11 +350,11 @@ export class MongoDialect extends AbstractDialect {
361
350
  result[op] = val;
362
351
  continue;
363
352
  }
364
- // String/pattern -> regex operators (8 variants including $like/$ilike)
365
- const regexEntry = REGEX_OP_MAP.get(op);
366
- if (regexEntry) {
367
- result['$regex'] = regexEntry.wrap(val);
368
- if (regexEntry.ci)
353
+ // The `$like` family, as the regex matching what its `LIKE` pattern matches on SQL.
354
+ const like = LIKE_OPS.get(op);
355
+ if (like) {
356
+ result['$regex'] = likeRegex(like.pattern(String(val)));
357
+ if (like.insensitive)
369
358
  result['$options'] = 'i';
370
359
  continue;
371
360
  }
@@ -429,17 +418,16 @@ export class MongoDialect extends AbstractDialect {
429
418
  if (!select && !exclude) {
430
419
  return {};
431
420
  }
432
- if (Array.isArray(select)) {
421
+ if (isSelectList(select)) {
433
422
  throw new UqlUsageError('raw $select is not supported on MongoDB');
434
423
  }
435
- const selectMap = asSelectMap(select);
436
424
  // Projected by column, not by field key; `normalizeId` maps them back on the way out.
437
- const projection = normalizeScalarFieldSelection(meta, selectMap, exclude).reduce((acc, key) => {
425
+ const projection = normalizeScalarFieldSelection(meta, select, exclude).reduce((acc, key) => {
438
426
  // A computed field writing SQL leaves the document nothing to project: refused asked for by
439
427
  // name, skipped swept in with the rest. A relation aggregate is on it by now, like any column.
440
428
  const field = meta.fields[key];
441
429
  if (field?.computed && !aggregateOf(field)) {
442
- if (selectMap && key in selectMap) {
430
+ if (select && key in select) {
443
431
  assertReadable(meta, key);
444
432
  }
445
433
  return acc;
@@ -450,7 +438,7 @@ export class MongoDialect extends AbstractDialect {
450
438
  // MongoDB returns `_id` unless it is explicitly excluded, so subtracting the primary key needs
451
439
  // `_id: 0` - the one inclusion/exclusion mix MongoDB allows - or `$exclude: { id: true }` would
452
440
  // have no effect at all.
453
- if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), selectMap, exclude)) {
441
+ if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), select, exclude)) {
454
442
  projection[ID_KEY] = 0;
455
443
  }
456
444
  return projection;
@@ -565,7 +553,7 @@ export class MongoDialect extends AbstractDialect {
565
553
  /** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
566
554
  aggregateKeys(entity, q) {
567
555
  const meta = getMeta(entity);
568
- const projected = normalizeScalarFieldSelection(meta, asSelectMap(q.$select), q.$exclude);
556
+ const projected = normalizeScalarFieldSelection(meta, isSelectList(q.$select) ? undefined : q.$select, q.$exclude);
569
557
  return [...projected, ...Object.keys(q.$sort ?? {})].filter((key) => aggregateOf(meta.fields[key]));
570
558
  }
571
559
  /**
@@ -138,35 +138,37 @@ export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
138
138
  */
139
139
  $between?: readonly [ExpandScalar<T>, ExpandScalar<T>];
140
140
  /**
141
- * whether a string begins with the given string (case sensitive).
141
+ * whether a string begins with the given text, taken literally (case sensitive).
142
142
  */
143
143
  $startsWith?: string;
144
144
  /**
145
- * whether a string begins with the given string (case insensitive).
145
+ * whether a string begins with the given text, taken literally (case insensitive).
146
146
  */
147
147
  $istartsWith?: string;
148
148
  /**
149
- * whether a string ends with the given string (case sensitive).
149
+ * whether a string ends with the given text, taken literally (case sensitive).
150
150
  */
151
151
  $endsWith?: string;
152
152
  /**
153
- * whether a string ends with the given string (case insensitive).
153
+ * whether a string ends with the given text, taken literally (case insensitive).
154
154
  */
155
155
  $iendsWith?: string;
156
156
  /**
157
- * whether a string is contained within the given string (case sensitive).
157
+ * whether a string contains the given text, taken literally (case sensitive).
158
158
  */
159
159
  $includes?: string;
160
160
  /**
161
- * whether a string is contained within the given string (case insensitive).
161
+ * whether a string contains the given text, taken literally (case insensitive).
162
162
  */
163
163
  $iincludes?: string;
164
164
  /**
165
- * whether a string fulfills the given pattern (case sensitive).
165
+ * whether a whole string matches the given pattern, the same on every engine: `%` is any run of
166
+ * characters, `_` any one, and `\` makes the next one literal; a last `\` with nothing after it to
167
+ * escape, `'John\'`, is refused (case sensitive).
166
168
  */
167
169
  $like?: string;
168
170
  /**
169
- * whether a string fulfills the given pattern (case insensitive).
171
+ * whether a whole string matches the given pattern, as `$like` reads it (case insensitive).
170
172
  */
171
173
  $ilike?: string;
172
174
  /**
@@ -63,11 +63,8 @@ export declare function whereEach<E>(keys: readonly FieldKey<E>[], valueOf: (key
63
63
  export declare function whereAnyOf<E>(clauses: QueryWhereArray<E>): QueryWhere<E>;
64
64
  /** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
65
65
  export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
66
- /**
67
- * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
68
- * narrowing cast: `Array.isArray` does not narrow `readonly` arrays out of a union.
69
- */
70
- export declare function asSelectMap<E>(select: QuerySelectValue<E> | undefined): QuerySelect<E> | undefined;
66
+ /** Whether `select` is the list form `raw()` fills, narrowing both ways, which `Array.isArray` does not for a `readonly` array. */
67
+ export declare function isSelectList<E>(select: QuerySelectValue<E> | undefined): select is readonly QueryRaw[];
71
68
  export declare function normalizeScalarFieldSelection<E>(meta: EntityMeta<E>, select?: QuerySelect<E>, exclude?: QueryExclude<E>): FieldKey<E>[];
72
69
  /** Type guard: checks whether a sort value is a vector similarity search. */
73
70
  export declare function isVectorSearch(value: unknown): value is QueryVectorSearch;
@@ -138,12 +138,9 @@ export function whereAnyOf(clauses) {
138
138
  export function idOnlyQuery(meta, q) {
139
139
  return { ...q, $select: keySet(meta.ids) };
140
140
  }
141
- /**
142
- * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
143
- * narrowing cast: `Array.isArray` does not narrow `readonly` arrays out of a union.
144
- */
145
- export function asSelectMap(select) {
146
- return Array.isArray(select) ? undefined : select;
141
+ /** Whether `select` is the list form `raw()` fills, narrowing both ways, which `Array.isArray` does not for a `readonly` array. */
142
+ export function isSelectList(select) {
143
+ return Array.isArray(select);
147
144
  }
148
145
  export function normalizeScalarFieldSelection(meta, select, exclude) {
149
146
  // A positive `$select` (the common case) wins outright and returns
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.83.0",
6
+ "version": "0.83.1",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"