uql-orm 0.26.3 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -0
- package/dist/cockroachdb/cockroachDialect.js +5 -0
- package/dist/dialect/abstractSqlDialect.d.ts +81 -40
- package/dist/dialect/abstractSqlDialect.js +317 -305
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +4 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +1 -3
- package/dist/dialect/queryContext.d.ts +7 -5
- package/dist/dialect/queryContext.js +11 -6
- package/dist/dialect/queryJoins.d.ts +44 -0
- package/dist/dialect/queryJoins.js +73 -0
- package/dist/dialect/vectorSqlDialect.d.ts +1 -1
- package/dist/http/query.js +10 -0
- package/dist/maria/mariaDialect.d.ts +2 -0
- package/dist/maria/mariaDialect.js +2 -0
- package/dist/mongo/mongoDialect.d.ts +14 -2
- package/dist/mongo/mongoDialect.js +56 -18
- package/dist/mongo/mongodbQuerier.js +6 -2
- package/dist/querier/abstractSqlQuerier.d.ts +11 -0
- package/dist/querier/abstractSqlQuerier.js +21 -0
- package/dist/sqlite/sqliteDialect.d.ts +3 -1
- package/dist/sqlite/sqliteDialect.js +5 -3
- package/dist/type/dialect.d.ts +11 -3
- package/dist/type/entity.d.ts +4 -4
- package/dist/type/index.d.ts +1 -0
- package/dist/type/index.js +1 -0
- package/dist/type/query.d.ts +15 -4
- package/dist/type/queryLock.d.ts +21 -0
- package/dist/type/queryLock.js +19 -0
- package/dist/type/utility.d.ts +9 -5
- package/dist/util/dialect.util.d.ts +1 -2
- package/dist/util/dialect.util.js +0 -3
- package/dist/util/relationQuery.util.d.ts +15 -3
- package/dist/util/relationQuery.util.js +37 -2
- package/dist/util/sql.util.d.ts +1 -2
- package/dist/util/sql.util.js +1 -12
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
</picture>
|
|
8
8
|
</a>
|
|
9
9
|
|
|
10
|
-
<h3>The
|
|
10
|
+
<h3>The ORM with type-safe JSON queries</h3>
|
|
11
11
|
|
|
12
|
-
<p>
|
|
12
|
+
<p>Queries are plain JSON values, typed to the leaf. Build them, store them, send them from the browser, and run them unchanged on every SQL database and MongoDB. And it is the <a href="https://uql-orm.dev/benchmark">fastest</a>.</p>
|
|
13
13
|
|
|
14
14
|
<p>
|
|
15
15
|
<a href="https://uql-orm.dev"><b>Website</b></a> ·
|
|
@@ -56,9 +56,9 @@ from the browser to the server. The same object runs on every supported database
|
|
|
56
56
|
- **Relations without N+1.** [`$populate`](https://uql-orm.dev/querying/relations) loads a to-many with one query for all parents, not one per parent. Nothing is lazy, so nothing fires behind your back in a serializer.
|
|
57
57
|
- **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.
|
|
58
58
|
- **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [virtual fields](https://uql-orm.dev/entities/virtual-fields) are sub-queries you can filter on, and a migration can be plain SQL.
|
|
59
|
-
- **Light.** Zero runtime dependencies,
|
|
59
|
+
- **Light.** Zero runtime dependencies, 269 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
60
60
|
- **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
|
|
61
|
-
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark):
|
|
61
|
+
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark): 206µs, against 644µs for the next closest and 2,236µs for the slowest.
|
|
62
62
|
|
|
63
63
|
## Get started
|
|
64
64
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var W=[];function F(B){for(let V of W)V(B)}function
|
|
1
|
+
var W=[];function F(B){for(let V of W)V(B)}function _(B){W.push(B);let V=W.length-1;return()=>{W.splice(V,1)}}class f extends Error{status;constructor(B,V){super(B);this.status=V;this.name="RequestError"}}function N(B,V){return G(B,{method:"get"},V)}function Q(B,V,I){let X=JSON.stringify(V);return G(B,{method:"post",body:X},I)}function J(B,V,I){let X=JSON.stringify(V);return G(B,{method:"patch",body:X},I)}function L(B,V,I){let X=JSON.stringify(V);return G(B,{method:"put",body:X},I)}function x(B,V){return G(B,{method:"delete"},V)}function T(B,V,I){let X=JSON.stringify(V);return G(B,{method:"QUERY",body:X},I)}function G(B,V,I){if(F({phase:"start",opts:I}),V.headers={accept:"application/json","content-type":"application/json",...I?.headers},I?.signal)V.signal=I.signal;return fetch(B,V).then((X)=>X.json().then((Z)=>{if(X.status>=200&&X.status<300)return F({phase:"success",opts:I}),Z;let Y=Z,M={message:Y?.error?.message??X.statusText,code:Y?.error?.code??X.status};throw F({phase:"error",error:M,opts:I}),new f(M.message,M.code)})).finally(()=>{F({phase:"complete",opts:I})})}function z(B){return B?Object.keys(B):[]}function K(B){let V=B.charAt(0).toLowerCase();for(let I=1;I<B.length;++I)V+=B[I]===B[I].toUpperCase()?"-"+B[I].toLowerCase():B[I];return V}var $={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},k=z($),b=new Map(k.filter((B)=>$[B].method==="GET"&&$[B].path!=="/:id").map((B)=>[$[B].path,B]));function D(B){return K(B.name)}var c=["$select","$populate","$exclude","$where","$sort"],v=new Set([...c,"$skip","$limit","hardDelete","count"]);function A(B){if(!B)return"";let V=new URLSearchParams;for(let X of z(B)){let Z=B[X];if(Z===void 0)continue;V.append(X,typeof Z==="object"&&Z!==null?JSON.stringify(Z):String(Z))}let I=V.toString();return I?`?${I}`:""}class C{basePath;defaults;constructor(B,V={}){this.basePath=B;this.defaults=V}findOneById(B,V,I,X){let Z=this.getBasePath(B),H=A(I);return N(`${Z}/${V}${H}`,this.buildOptions(X))}findOne(B,V,I){return this.read(`${this.getBasePath(B)}${$.findOne.path}`,V,I)}findMany(B,V,I){let X={...V};if(I?.count)X.count=!0;return this.read(this.getBasePath(B),X,I)}async findManyAndCount(B,V,I){let X=await this.findMany(B,V,{...I,count:!0});if(typeof X.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...X,count:X.count}}count(B,V,I){return this.read(`${this.getBasePath(B)}${$.count.path}`,V,I)}insertOne(B,V,I){let X=this.getBasePath(B);return Q(X,V,this.buildOptions(I))}insertMany(B,V,I){let X=this.getBasePath(B);return Q(`${X}${$.insertMany.path}`,V,this.buildOptions(I))}updateOneById(B,V,I,X){let Z=this.getBasePath(B);return J(`${Z}/${V}`,I,this.buildOptions(X))}updateMany(B,V,I,X){let Z=this.getBasePath(B),H=A(V);return J(`${Z}${H}`,I,this.buildOptions(X))}saveOne(B,V,I){let X=this.getBasePath(B);return L(X,V,this.buildOptions(I))}saveMany(B,V,I){let X=this.getBasePath(B);return L(`${X}${$.saveMany.path}`,V,this.buildOptions(I))}deleteOneById(B,V,I={}){let X=this.getBasePath(B),Z=I.hardDelete?A({hardDelete:I.hardDelete}):"";return x(`${X}/${V}${Z}`,this.buildOptions(I))}deleteMany(B,V,I={}){let X=this.getBasePath(B),Z=A(I.hardDelete?{...V,hardDelete:I.hardDelete}:V);return x(`${X}${Z}`,this.buildOptions(I))}getBasePath(B){return`${this.basePath}/${D(B)}`}read(B,V,I){if(this.defaults.readMethod==="QUERY")return T(B,V??{},this.buildOptions(I));return N(`${B}${A(V)}`,this.buildOptions(I))}buildOptions(B){if(!this.defaults.headers&&!B?.headers)return B;return{...B,headers:{...this.defaults.headers,...B?.headers}}}}var P={getQuerier:()=>new C("/api")};function y(B){P=B}function g(){return P}function p(){return g().getQuerier()}export{y as setQuerierPool,x as remove,T as query,L as put,Q as post,J as patch,_ as on,F as notify,g as getQuerierPool,p as getQuerier,N as get,f as RequestError,C as HttpQuerier};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=63F5785EAD0E521C64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=uql-browser.min.js.map
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"import type { FieldKey, FieldOptions } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/** Whether `obj` has at least two enumerable keys. */\nexport function hasMultipleKeys(obj: object): boolean {\n let count = 0;\n for (const _ in obj) {\n if (++count > 1) return true;\n }\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey(obj: object, pred: (key: string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */\nexport function someValue(obj: object, pred: (value: unknown) => boolean): boolean {\n return someKey(obj, (key) => pred((obj as Record<string, unknown>)[key]));\n}\n\nconst isOperatorKey = (key: string) => key.startsWith('$');\n\n/**\n * Whether `value` is a non-empty object whose keys are query/update operators (`$eq`, `$push`, ...).\n * The single source of this test: the SQL dialects, the MongoDB dialect and the `$elemMatch` walker\n * all classify operator objects with it, and they used to disagree about `{}`.\n */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */\nexport function isOperatorOnlyObject(value: unknown): value is Record<string, unknown> {\n return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));\n}\n\nexport function getKeys<T extends object>(obj: T): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\nexport function getFieldKeys<E>(\n fields: {\n [K in FieldKey<E>]?: FieldOptions;\n },\n): FieldKey<E>[] {\n return getKeys(fields).filter((field) => fields[field]!.eager ?? true);\n}\n",
|
|
8
8
|
"export function kebabCase(val: string): string {\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n resp += val[i] === val[i].toUpperCase() ? '-' + val[i].toLowerCase() : val[i];\n }\n return resp;\n}\n\nexport function upperFirst(text: string): string {\n if (!text) return text;\n return text[0].toUpperCase() + text.slice(1);\n}\n\nexport function lowerFirst(text: string): string {\n if (!text) return text;\n return text[0].toLowerCase() + text.slice(1);\n}\n\nexport function snakeCase(val: string): string {\n if (val === null || val === undefined) return val as string;\n if (!val) return '';\n let resp = val.charAt(0).toLowerCase();\n for (let i = 1; i < val.length; ++i) {\n const char = val[i];\n const charLower = char.toLowerCase();\n if (char !== charLower && char === char.toUpperCase()) {\n resp += '_' + charLower;\n } else {\n resp += char;\n }\n }\n return resp;\n}\n\n/**\n * Convert a string to PascalCase (UpperCamelCase).\n * @example 'user_profile' -> 'UserProfile'\n * @example 'some-text' -> 'SomeText'\n */\nexport function pascalCase(str: string): string {\n if (!str) return '';\n return str\n .split(/[_\\s-]+/)\n .map((word) => {\n // Lower-casing the rest is only right for a word that carries no case of its own: it turns\n // `USER_ID` into `UserId`, but it also turns `tenantId` into `Tenantid`.\n const rest = word === word.toUpperCase() ? word.slice(1).toLowerCase() : word.slice(1);\n return word.charAt(0).toUpperCase() + rest;\n })\n .join('');\n}\n\n/**\n * Convert a string to camelCase.\n * @example 'user_profile' -> 'userProfile'\n * @example 'SomeText' -> 'someText'\n */\nexport function camelCase(str: string): string {\n const pascal = pascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/**\n * Simple singularize function for English words.\n * @example 'users' -> 'user'\n * @example 'categories' -> 'category'\n */\nexport function singularize(name: string): string {\n if (!name) return '';\n if (name.endsWith('ies')) {\n return name.slice(0, -3) + 'y';\n }\n if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) {\n return name.slice(0, -2);\n }\n if (name.endsWith('s') && !name.endsWith('ss')) {\n return name.slice(0, -1);\n }\n return name;\n}\n\n/**\n * Simple pluralize function for English words.\n * @example 'user' -> 'users'\n * @example 'category' -> 'categories'\n */\nexport function pluralize(name: string): string {\n if (!name) return '';\n if (name.endsWith('y') && name.length > 1 && !/[aeiou]/.test(name[name.length - 2])) {\n return name.slice(0, -1) + 'ies';\n }\n if (name.endsWith('s') || name.endsWith('x') || name.endsWith('z') || name.endsWith('ch') || name.endsWith('sh')) {\n return name + 'es';\n }\n return name + 's';\n}\n",
|
|
9
9
|
"import type { Type, UniversalQuerier } from '../type/index.js';\n// the specific util modules, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\nimport { kebabCase } from '../util/string.util.js';\n\ntype RouteShape = {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n readonly path: '' | `/${string}`;\n};\n\n/**\n * Single source of truth for the CRUD-over-HTTP surface, shared by server adapters and the browser client.\n * Keys are constrained to {@link UniversalQuerier} method names, so renaming a querier method\n * (or routing a non-existent one) is a compile error.\n */\nexport const CRUD_ROUTES = {\n findMany: { method: 'GET', path: '' },\n findOne: { method: 'GET', path: '/one' },\n count: { method: 'GET', path: '/count' },\n findOneById: { method: 'GET', path: '/:id' },\n insertOne: { method: 'POST', path: '' },\n insertMany: { method: 'POST', path: '/many' },\n saveOne: { method: 'PUT', path: '' },\n saveMany: { method: 'PUT', path: '/many' },\n updateMany: { method: 'PATCH', path: '' },\n updateOneById: { method: 'PATCH', path: '/:id' },\n deleteOneById: { method: 'DELETE', path: '/:id' },\n deleteMany: { method: 'DELETE', path: '' },\n} as const satisfies Partial<Record<keyof UniversalQuerier, RouteShape>>;\n\nexport type CrudOperation = keyof typeof CRUD_ROUTES;\n\nexport type CrudRoute = (typeof CRUD_ROUTES)[CrudOperation];\n\n/**\n * `QUERY` (RFC 10008) is an alternate transport for the read operations: same semantics as the\n * GET routes, but the JSON query travels in the request body instead of the query string,\n * avoiding URL-length limits for large queries.\n */\nexport type HttpMethod = CrudRoute['method'] | 'QUERY';\n\nconst CRUD_OPS = getKeys(CRUD_ROUTES);\n\n// derived from CRUD_ROUTES (the literal-path GET routes) so the sub-paths live in exactly one place\nconst QUERY_READ_OPS: ReadonlyMap<string, CrudOperation> = new Map(\n CRUD_OPS.filter((op) => CRUD_ROUTES[op].method === 'GET' && CRUD_ROUTES[op].path !== '/:id').map((op) => [\n CRUD_ROUTES[op].path,\n op,\n ]),\n);\n\n/**\n * URL segment for an entity, e.g. `entityPath(UserProfile) === 'user-profile'`.\n */\nexport function entityPath<E>(entity: Type<E>): string {\n return kebabCase(entity.name);\n}\n\nexport type RouteMatch = {\n readonly op: CrudOperation;\n /**\n * the resolved transport method - differs from the op's canonical route method for QUERY.\n */\n readonly method: HttpMethod;\n readonly id?: string;\n};\n\n/**\n * Resolve a (method, sub-path) pair to a CRUD operation. Literal sub-paths win over `:id`.\n */\nexport function matchRoute(method: string, subPath: string | undefined): RouteMatch | undefined {\n const raw = method.toUpperCase();\n const literal = subPath === undefined ? '' : `/${subPath}`;\n if (raw === 'QUERY') {\n const op = QUERY_READ_OPS.get(literal);\n return op ? { op, method: 'QUERY' } : undefined;\n }\n // HEAD reads like GET per HTTP semantics; the server runtime omits the response body\n const verb = raw === 'HEAD' ? 'GET' : raw;\n let idOp: CrudOperation | undefined;\n for (const op of CRUD_OPS) {\n const route = CRUD_ROUTES[op];\n if (route.method !== verb) {\n continue;\n }\n if (route.path === literal) {\n return { op, method: route.method };\n }\n if (route.path === '/:id') {\n idOp = op;\n }\n }\n return idOp && subPath !== undefined ? { op: idOp, method: CRUD_ROUTES[idOp].method, id: subPath } : undefined;\n}\n\nexport type RequestSuccessResponse<E> = {\n data: E;\n count?: number;\n};\n\nexport type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {\n count: number;\n};\n\nexport type RequestErrorResponse = {\n readonly error: {\n readonly message: string;\n readonly code: number;\n };\n};\n\n/**\n * Map a thrown error to the wire error envelope. Honors a numeric `status` on the error\n * (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.\n */\nexport function toErrorResponse(err: unknown): { status: number; body: RequestErrorResponse } {\n const status = err instanceof Error && 'status' in err && typeof err.status === 'number' ? err.status : 500;\n const message = err instanceof Error ? err.message : 'Internal Server Error';\n return { status, body: { error: { message, code: status } } };\n}\n",
|
|
10
|
-
"import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
|
|
10
|
+
"import type { Query, QueryOptions } from '../type/index.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys } from '../util/object.util.js';\n\nconst JSON_QUERY_KEYS = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([...JSON_QUERY_KEYS, '$skip', '$limit', 'hardDelete', 'count'] satisfies (\n | keyof Query<unknown>\n | keyof Pick<QueryOptions, 'hardDelete'>\n | 'count'\n)[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof Query<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): Query<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of JSON_QUERY_KEYS) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n\n if (query['$skip']) {\n query['$skip'] = Number(query['$skip']);\n }\n if (query['$limit']) {\n query['$limit'] = Number(query['$limit']);\n }\n\n return query as Query<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n",
|
|
11
11
|
"import {\n CRUD_ROUTES,\n entityPath,\n type HttpMethod,\n type RequestCountedSuccessResponse,\n type RequestSuccessResponse,\n} from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type { IdValue, Query, QueryOne, QueryOptions, QuerySearch, Type, UpdatePayload } from '../../type/index.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n};\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n findOneById<E extends object>(\n entity: Type<E>,\n id: IdValue<E>,\n q?: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<E | undefined>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n findOne<E extends object>(\n entity: Type<E>,\n q: QueryOne<E>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<E | undefined>> {\n return this.read<E | undefined>(`${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`, q, opts);\n }\n\n findMany<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<E[]>> {\n const data: Query<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<E[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<E extends object>(\n entity: Type<E>,\n q: Query<E>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<E[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.insertMany.path}`, payload, this.buildOptions(opts));\n }\n\n updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${id}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: E, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: E[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<IdValue<E>[]>(`${basePath}${CRUD_ROUTES.saveMany.path}`, payload, this.buildOptions(opts));\n }\n\n deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${id}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${entityPath(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
|
|
12
12
|
"import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
|
|
13
13
|
],
|
|
14
|
-
"mappings": "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
|
|
15
|
-
"debugId": "
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "63F5785EAD0E521C64756E2164756E21",
|
|
16
16
|
"names": []
|
|
17
17
|
}
|
|
@@ -22,6 +22,11 @@ export declare class CockroachDialect extends PgLikeSqlDialect {
|
|
|
22
22
|
* not supported" (both verified on v26.2), so neither is offered here.
|
|
23
23
|
*/
|
|
24
24
|
protected readonly indexFeatures: Set<IndexFeature>;
|
|
25
|
+
/**
|
|
26
|
+
* `noKeyUpdate`/`keyShare` are omitted on purpose, not by oversight: CockroachDB parses both and
|
|
27
|
+
* treats them as aliases of `FOR UPDATE`/`FOR SHARE`, so offering them would hand back a stronger
|
|
28
|
+
* lock than was asked for, with nothing signalling it.
|
|
29
|
+
*/
|
|
25
30
|
/**
|
|
26
31
|
* CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
|
|
27
32
|
* vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
|
|
@@ -27,6 +27,11 @@ export class CockroachDialect extends PgLikeSqlDialect {
|
|
|
27
27
|
* not supported" (both verified on v26.2), so neither is offered here.
|
|
28
28
|
*/
|
|
29
29
|
indexFeatures = new Set(['expression', 'partial', 'include']);
|
|
30
|
+
/**
|
|
31
|
+
* `noKeyUpdate`/`keyShare` are omitted on purpose, not by oversight: CockroachDB parses both and
|
|
32
|
+
* treats them as aliases of `FOR UPDATE`/`FOR SHARE`, so offering them would hand back a stronger
|
|
33
|
+
* lock than was asked for, with nothing signalling it.
|
|
34
|
+
*/
|
|
30
35
|
/**
|
|
31
36
|
* CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
|
|
32
37
|
* vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager,
|
|
1
|
+
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
2
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
3
3
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
4
|
+
import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
|
|
4
5
|
/** How a column's values are bound: see {@link AbstractSqlDialect.persistKind}. */
|
|
5
6
|
type PersistKind = 'plain' | 'json' | 'vector';
|
|
6
7
|
/** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
|
|
@@ -34,7 +35,8 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
34
35
|
* straight into `ctx`'s own values array - shared by reference, not copied - so `addValue` numbers
|
|
35
36
|
* its placeholder correctly against the real query from the start; a fresh, empty array would
|
|
36
37
|
* instead number from `1` regardless of how many values `ctx` already has, misnumbering every
|
|
37
|
-
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty.
|
|
38
|
+
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty. Generated aliases are
|
|
39
|
+
* shared for the same reason - see {@link SqlQueryContext}.
|
|
38
40
|
*/
|
|
39
41
|
protected buildFragment(ctx: QueryContext, build: (fragmentCtx: QueryContext) => void): string;
|
|
40
42
|
addValue(values: unknown[], value: unknown): string;
|
|
@@ -51,7 +53,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
51
53
|
normalizeValues(values: unknown[] | undefined): unknown[] | undefined;
|
|
52
54
|
placeholder(_index: number): string;
|
|
53
55
|
returningId<E>(entity: Type<E>): string;
|
|
54
|
-
search<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions): void;
|
|
56
|
+
search<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
55
57
|
selectFields<E>(ctx: QueryContext, entity: Type<E>, select: QuerySelectValue<E> | undefined, opts?: QuerySelectOptions, exclude?: QueryExclude<E>): void;
|
|
56
58
|
/**
|
|
57
59
|
* The expression a scalar field is read through, the plain column by default. MariaDB reads a
|
|
@@ -65,19 +67,11 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
65
67
|
* than inheriting another engine's syntax.
|
|
66
68
|
*/
|
|
67
69
|
protected appendTextSearch<E>(_ctx: QueryContext, _entity: Type<E>, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
|
|
68
|
-
select<E>(ctx: QueryContext, entity: Type<E>,
|
|
70
|
+
select<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
71
|
+
/** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
|
|
69
72
|
private resolveRelationAwarePrefix;
|
|
70
|
-
protected selectRelationFields
|
|
71
|
-
|
|
72
|
-
}): void;
|
|
73
|
-
protected selectRelationJoins<E>(ctx: QueryContext, entity: Type<E>, select: QuerySelect<E> | undefined, populate: QueryPopulate<E> | undefined, opts?: {
|
|
74
|
-
prefix?: string;
|
|
75
|
-
}): void;
|
|
76
|
-
/**
|
|
77
|
-
* Iterates over joinable (11/m1) relations for a given select, resolving shared metadata.
|
|
78
|
-
* Used by both `selectRelationFields` and `selectRelationJoins` to avoid duplicated iteration logic.
|
|
79
|
-
*/
|
|
80
|
-
private forEachJoinableRelation;
|
|
73
|
+
protected selectRelationFields(ctx: QueryContext, joins: QueryJoins): void;
|
|
74
|
+
protected selectRelationJoins<E>(ctx: QueryContext, meta: EntityMeta<E>, tableName: string, joins: QueryJoins): void;
|
|
81
75
|
where<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
82
76
|
/** Renders a `$where` tree without applying entity filters (used for same-scope `$and`/`$or` recursion). */
|
|
83
77
|
protected renderWhere<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
@@ -87,39 +81,57 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
87
81
|
private readonly escapedColumns;
|
|
88
82
|
private static readonly NEGATE_OP_MAP;
|
|
89
83
|
private static readonly COMPARE_OP_MAP;
|
|
90
|
-
private static readonly LIKE_OP_MAP;
|
|
91
84
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
85
|
+
* Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
|
|
86
|
+
* Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
|
|
87
|
+
* two can never drift apart - and neither one decides case folding, which is
|
|
88
|
+
* {@link caseInsensitiveMatch}'s single call.
|
|
89
|
+
*/
|
|
90
|
+
private static readonly LIKE_OPS;
|
|
91
|
+
/**
|
|
92
|
+
* How this engine matches case-insensitively. One decision, not two: folding the pattern while the
|
|
93
|
+
* comparison leaves the column alone matches neither case, which is what `$istartsWith: 'Some'`
|
|
94
|
+
* used to do wherever `LIKE` is case-sensitive.
|
|
95
|
+
*
|
|
96
|
+
* - `ilike`: the engine has a case-insensitive operator (`ILIKE`), so the pattern goes through as written.
|
|
97
|
+
* - `native`: plain `LIKE` already ignores case (SQLite, for ASCII). Folding the pattern in JS would
|
|
98
|
+
* only break the non-ASCII characters the engine cannot fold anyway - `'É'` would become an `'é'`
|
|
99
|
+
* that matches nothing.
|
|
100
|
+
* - `fold`: nothing ignores case on its own, so both sides are lowered explicitly. Not indexable as
|
|
101
|
+
* such; an expression index over `LOWER(column)` is what makes it so.
|
|
95
102
|
*/
|
|
96
|
-
|
|
97
|
-
/**
|
|
103
|
+
protected readonly caseInsensitiveMatch: 'ilike' | 'native' | 'fold';
|
|
104
|
+
/**
|
|
105
|
+
* A `$like`-family condition, or `undefined` when `op` is not one of them. Shared by columns and
|
|
106
|
+
* JSON paths, and the only place a pattern is folded - always together with the column it is
|
|
107
|
+
* compared against.
|
|
108
|
+
*/
|
|
109
|
+
protected likeCondition(ctx: QueryContext, operand: string, op: string, val: unknown): string | undefined;
|
|
110
|
+
/** Builds `prefix.column` from an already-resolved field, through the same memo writes use. */
|
|
98
111
|
private columnWithPrefix;
|
|
99
112
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
113
|
+
* The SQL a field comparison reads its left-hand side from. A virtual field builds its expression
|
|
114
|
+
* as text rather than appending it, so every operator gets a real operand to wrap - `LOWER(...)`,
|
|
115
|
+
* `NOT (... <=> ...)` - instead of having to fall back to a form that takes none.
|
|
102
116
|
*/
|
|
103
|
-
protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string
|
|
104
|
-
private appendFieldSql;
|
|
117
|
+
protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string;
|
|
105
118
|
compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, op: K, val: QueryWhereFieldOperatorMap<E>[K], opts?: QueryOptions): void;
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
120
|
+
* `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, or
|
|
121
|
+
* `undefined` when `op` is not one of them.
|
|
109
122
|
*
|
|
110
123
|
* One implementation for three callers that each had their own: a WHERE column, a HAVING aggregate
|
|
111
|
-
* expression, and a `$size` count (
|
|
112
|
-
*
|
|
113
|
-
* `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of
|
|
114
|
-
* other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
|
|
124
|
+
* expression, and a `$size` count (whose expression is already in the context, so it passes an
|
|
125
|
+
* empty operand). They previously disagreed - HAVING carried a second comparison-operator map and
|
|
126
|
+
* threw `unsupported HAVING operator` on the `$like` that `QueryHavingMap` accepts, and neither of
|
|
127
|
+
* the other two turned `$eq: null` into `IS NULL` the way the WHERE path does.
|
|
115
128
|
*
|
|
116
129
|
* The operators kept out are the ones that need more than an operand: `$not` recurses through the
|
|
117
130
|
* entity, and `$all`/`$size`/`$elemMatch` address a JSON document.
|
|
118
131
|
*/
|
|
119
|
-
protected
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
private appendInNin;
|
|
132
|
+
protected operatorCondition(ctx: QueryContext, operand: string, op: string, val: unknown): string | undefined;
|
|
133
|
+
/** {@link operatorCondition}, appended; `false` when `op` needs more than an operand. */
|
|
134
|
+
protected appendOperatorCondition(ctx: QueryContext, operand: string, op: string, val: unknown): boolean;
|
|
123
135
|
/**
|
|
124
136
|
* Build a comparison condition for a JSON field.
|
|
125
137
|
* Used by both `$elemMatch` and dot-notation paths.
|
|
@@ -181,9 +193,36 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
181
193
|
* dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
|
|
182
194
|
*/
|
|
183
195
|
protected jsonScalarParam(ctx: QueryContext, value: unknown): string;
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
/** {@link resolveOperandField}, appended. */
|
|
197
|
+
getComparisonKey<E>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, opts?: QueryOptions): void;
|
|
198
|
+
sort<E>(ctx: QueryContext, entity: Type<E>, sort: QuerySortMap<E> | undefined, opts?: QuerySortOptions): void;
|
|
199
|
+
/**
|
|
200
|
+
* Walks `$sort` against the metadata of the entity each level addresses, rather than flattening it
|
|
201
|
+
* to dotted strings and reading every key off the root: only that way does a related column resolve
|
|
202
|
+
* through its own `@Field({ name })`, and only that way is `tax.category` the one alias the join
|
|
203
|
+
* carries instead of two quoted identifiers.
|
|
204
|
+
*/
|
|
205
|
+
private collectSortTerms;
|
|
206
|
+
/** The join an `ORDER BY` term addresses, or why the statement cannot order by it. */
|
|
207
|
+
private resolveSortJoin;
|
|
208
|
+
/**
|
|
209
|
+
* The `ORDER BY` operand for one key. A key that is not a column of `meta` - a virtual field, a
|
|
210
|
+
* `raw()` projection - is an output alias, which is never table-qualified and needs no resolving.
|
|
211
|
+
*/
|
|
212
|
+
private sortColumn;
|
|
186
213
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
214
|
+
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
215
|
+
readonly supportsRowLocks: boolean;
|
|
216
|
+
/** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
|
|
217
|
+
readonly supportsLockOf: boolean;
|
|
218
|
+
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
219
|
+
assertLockSupported<E>(entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
220
|
+
/**
|
|
221
|
+
* The trailing `FOR UPDATE`. Narrowing to the queried table is not a nicety once a relation is
|
|
222
|
+
* joined: Postgres refuses a bare `FOR UPDATE` over the nullable side of an outer join outright,
|
|
223
|
+
* and the other engines quietly widen the lock to the joined rows.
|
|
224
|
+
*/
|
|
225
|
+
protected appendLock<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
187
226
|
count<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
|
|
188
227
|
/** `$group` aggregate operator → SQL function name. An allowlist, not a formatter: the op key
|
|
189
228
|
* comes from query data, so anything outside this map must be rejected rather than passed
|
|
@@ -191,7 +230,9 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
191
230
|
private static readonly AGGREGATE_FN_MAP;
|
|
192
231
|
aggregate<E, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(ctx: QueryContext, entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): void;
|
|
193
232
|
/**
|
|
194
|
-
* ORDER BY for aggregate queries - handles both entity-field and alias references.
|
|
233
|
+
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
234
|
+
* statement has no joins to address, so a relation key is rejected rather than emitted as an alias
|
|
235
|
+
* nothing defines.
|
|
195
236
|
*/
|
|
196
237
|
private aggregateSort;
|
|
197
238
|
protected having(ctx: QueryContext, having: QueryHavingMap, aggregateExpressions: Record<string, string>): void;
|
|
@@ -239,7 +280,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
239
280
|
protected getUpsertUpdateAssignments<E>(ctx: QueryContext, meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], callback?: (columnName: string) => string): string;
|
|
240
281
|
protected getUpsertConflictPathsStr<E>(meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>): string;
|
|
241
282
|
delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
|
|
242
|
-
escapeId(val: string, forbidQualified?: boolean, addDot?: boolean): string;
|
|
283
|
+
escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
243
284
|
/**
|
|
244
285
|
* Bind one persisted value, classifying its column on the spot. Dialects override
|
|
245
286
|
* {@link appendJsonValue} and {@link appendVectorValue} rather than this, so the chain runs once per
|
|
@@ -372,6 +413,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
372
413
|
* metadata is shared between dialects while this result is not, since `escapeIdChar` and the naming
|
|
373
414
|
* strategy differ. Weakly keyed so a transient entity's metadata stays collectable.
|
|
374
415
|
*/
|
|
416
|
+
private escapedColumnOf;
|
|
375
417
|
private escapedColumnName;
|
|
376
418
|
private escapedColumn;
|
|
377
419
|
/** As {@link escapedColumn}, but qualified by the query alias when the parent is nested. */
|
|
@@ -414,7 +456,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
414
456
|
*/
|
|
415
457
|
protected get neOp(): string;
|
|
416
458
|
protected neExpr(field: string, ph: string): string;
|
|
417
|
-
protected ilikeExpr(f: string, ph: string): string;
|
|
418
459
|
/**
|
|
419
460
|
* Formats an IN/NOT IN expression, binding each value individually.
|
|
420
461
|
* Postgres overrides to use `= ANY($1)` / `<> ALL($1)` with a single array parameter.
|