uql-orm 0.77.0 → 0.77.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/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/d1/d1Querier.js +2 -1
- package/dist/d1/d1SqliteDialect.js +2 -1
- package/dist/dialect/abstractDialect.d.ts +7 -1
- package/dist/dialect/abstractDialect.js +17 -5
- package/dist/dialect/abstractSqlDialect.d.ts +4 -4
- package/dist/dialect/abstractSqlDialect.js +30 -26
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -3
- package/dist/dialect/pgLikeSqlDialect.js +1 -3
- package/dist/dialect/queryJoins.js +6 -5
- package/dist/dialect/vectorSqlDialect.js +2 -1
- package/dist/http/query.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/maria/mariaDialect.js +2 -2
- package/dist/mongo/mongoDialect.d.ts +0 -6
- package/dist/mongo/mongoDialect.js +21 -29
- package/dist/mongo/mongodbQuerier.js +0 -1
- package/dist/mssql/mssqlDialect.d.ts +1 -5
- package/dist/mssql/mssqlDialect.js +4 -11
- package/dist/querier/abstractQuerier.d.ts +8 -1
- package/dist/querier/abstractQuerier.js +32 -14
- package/dist/querier/abstractSqlQuerier.d.ts +0 -5
- package/dist/querier/abstractSqlQuerier.js +4 -17
- package/dist/querier/queryError.d.ts +1 -27
- package/dist/querier/queryError.js +3 -28
- package/dist/sqlite/sqliteDialect.js +0 -2
- package/dist/type/dialect.d.ts +18 -6
- package/dist/type/queryAggregate.js +2 -1
- package/dist/type/queryLock.js +2 -1
- package/dist/type/queryWhere.d.ts +7 -6
- package/dist/type/utility.d.ts +8 -0
- package/dist/util/dialect.util.js +14 -13
- package/dist/util/relationQuery.util.js +4 -3
- package/dist/util/uqlError.d.ts +39 -0
- package/dist/util/uqlError.js +35 -0
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +7 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LoggerWrapper } from '../util/logger.js';
|
|
2
|
+
import { type QueryErrorKind } from '../util/uqlError.js';
|
|
2
3
|
/**
|
|
3
4
|
* A driver error tagged by {@link enrichError}: `query` always, `values` only when the logger already
|
|
4
5
|
* surfaces them, since they can carry PII or tokens into whatever serializes the error.
|
|
@@ -7,33 +8,6 @@ export interface QueryError extends Error {
|
|
|
7
8
|
query?: string;
|
|
8
9
|
values?: unknown[];
|
|
9
10
|
}
|
|
10
|
-
/**
|
|
11
|
-
* What a failed query ran into, named the same on every engine. `retryable` is a deadlock, a
|
|
12
|
-
* serialization failure, a lock timeout or a busy database: the transaction can simply run again.
|
|
13
|
-
*/
|
|
14
|
-
export type QueryErrorKind = 'uniqueViolation' | 'foreignKeyViolation' | 'notNullViolation' | 'checkViolation' | 'optimisticLock' | 'retryable';
|
|
15
|
-
/**
|
|
16
|
-
* Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
|
|
17
|
-
* or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
|
|
18
|
-
* where there is no row left. `status` is what an HTTP transport answers with.
|
|
19
|
-
*/
|
|
20
|
-
export declare class UqlOptimisticLockError extends Error {
|
|
21
|
-
readonly expected: unknown;
|
|
22
|
-
readonly actual: unknown;
|
|
23
|
-
name: string;
|
|
24
|
-
readonly status = 409;
|
|
25
|
-
constructor(message: string, expected: unknown, actual: unknown);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Thrown where a write cannot carry the optimistic lock: an update payload without its version, or a
|
|
29
|
-
* method with no version to match. A `TypeError` still, since the caller used the API wrong, but one
|
|
30
|
-
* carrying the `status` an HTTP transport answers with - the request is malformed, not the server's
|
|
31
|
-
* failure, and an untyped client is exactly who reaches this.
|
|
32
|
-
*/
|
|
33
|
-
export declare class UqlLockUsageError extends TypeError {
|
|
34
|
-
name: string;
|
|
35
|
-
readonly status = 400;
|
|
36
|
-
}
|
|
37
11
|
/**
|
|
38
12
|
* Names what `err` ran into on any engine, or `undefined` for anything else. Pure: the error is only
|
|
39
13
|
* read, so it works on any driver error, whether or not a querier saw it first.
|
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
|
|
3
|
-
* or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
|
|
4
|
-
* where there is no row left. `status` is what an HTTP transport answers with.
|
|
5
|
-
*/
|
|
6
|
-
export class UqlOptimisticLockError extends Error {
|
|
7
|
-
expected;
|
|
8
|
-
actual;
|
|
9
|
-
name = 'UqlOptimisticLockError';
|
|
10
|
-
status = 409;
|
|
11
|
-
constructor(message, expected, actual) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.expected = expected;
|
|
14
|
-
this.actual = actual;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Thrown where a write cannot carry the optimistic lock: an update payload without its version, or a
|
|
19
|
-
* method with no version to match. A `TypeError` still, since the caller used the API wrong, but one
|
|
20
|
-
* carrying the `status` an HTTP transport answers with - the request is malformed, not the server's
|
|
21
|
-
* failure, and an untyped client is exactly who reaches this.
|
|
22
|
-
*/
|
|
23
|
-
export class UqlLockUsageError extends TypeError {
|
|
24
|
-
name = 'UqlLockUsageError';
|
|
25
|
-
status = 400;
|
|
26
|
-
}
|
|
1
|
+
import { UqlOptimisticLockError, UqlUsageError } from '../util/uqlError.js';
|
|
27
2
|
/** Postgres, CockroachDB, PGlite and Neon in `code`; Bun SQL in `errno`. */
|
|
28
3
|
const SQLSTATE_KINDS = new Map([
|
|
29
4
|
['23505', 'uniqueViolation'],
|
|
@@ -78,8 +53,8 @@ export function queryErrorKind(err) {
|
|
|
78
53
|
if (typeof err !== 'object' || err === null) {
|
|
79
54
|
return undefined;
|
|
80
55
|
}
|
|
81
|
-
if (err instanceof UqlOptimisticLockError) {
|
|
82
|
-
return
|
|
56
|
+
if (err instanceof UqlOptimisticLockError || err instanceof UqlUsageError) {
|
|
57
|
+
return err.kind;
|
|
83
58
|
}
|
|
84
59
|
const { code, errno, number, errorLabels, message } = err;
|
|
85
60
|
const text = typeof message === 'string' ? message : '';
|
|
@@ -34,9 +34,7 @@ export const SQLITE_FEATURES = {
|
|
|
34
34
|
serverSideCursors: false,
|
|
35
35
|
correlatedWrites: true,
|
|
36
36
|
rowLocks: false,
|
|
37
|
-
rowLockWithWindow: true,
|
|
38
37
|
nullsOrdering: 'clause',
|
|
39
|
-
rowLockOf: true,
|
|
40
38
|
textScoreIndexes: false,
|
|
41
39
|
orderedUpsertReturning: true,
|
|
42
40
|
orderedJsonAggregates: true,
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -135,15 +135,27 @@ export interface DialectFeatures {
|
|
|
135
135
|
* such a write reads the ids of the rows it names first.
|
|
136
136
|
*/
|
|
137
137
|
readonly correlatedWrites: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* What the engine's row locks can do, or `false` where it has none: the SQLite family locks the
|
|
140
|
+
* database and MongoDB has no row lock at all, so both refuse `$lock` rather than ignoring it. One
|
|
141
|
+
* value rather than a flag each, since the details mean nothing without a lock.
|
|
142
|
+
*/
|
|
143
|
+
readonly rowLocks: RowLockFeatures | false;
|
|
144
|
+
}
|
|
145
|
+
/** How a dialect spells a row lock, once {@link DialectFeatures.rowLocks} says it has one. */
|
|
146
|
+
export interface RowLockFeatures {
|
|
147
|
+
/** Whether a lock can be narrowed to one table of a join, `FOR UPDATE OF`, which MariaDB lacks. */
|
|
148
|
+
readonly of: boolean;
|
|
149
|
+
/** Whether the lock may share a statement with a window function, which the Postgres family refuses. */
|
|
150
|
+
readonly withWindow: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Where the lock is spelled: after the statement (`FOR UPDATE`), or as a hint on the table it reads
|
|
153
|
+
* (`WITH (UPDLOCK)`, SQL Server). A dialect's `lockHint` states the hint itself.
|
|
154
|
+
*/
|
|
155
|
+
readonly placement: 'suffix' | 'tableHint';
|
|
138
156
|
}
|
|
139
157
|
/** What a SQL engine can do beyond {@link DialectFeatures}, read where a statement is built. */
|
|
140
158
|
export interface SqlDialectFeatures extends DialectFeatures {
|
|
141
|
-
/** Whether the engine has row locks at all. The SQLite family locks the database instead. */
|
|
142
|
-
readonly rowLocks: boolean;
|
|
143
|
-
/** Whether `FOR UPDATE` may share a statement with a window function, which the Postgres family refuses. */
|
|
144
|
-
readonly rowLockWithWindow: boolean;
|
|
145
|
-
/** Whether a lock can be narrowed to one table of a join, `FOR UPDATE OF`, which MariaDB lacks. */
|
|
146
|
-
readonly rowLockOf: boolean;
|
|
147
159
|
/**
|
|
148
160
|
* How a `$sort` states where nulls land: the `NULLS FIRST/LAST` clause, a leading `IS NULL` term
|
|
149
161
|
* (MySQL, MariaDB), or a leading `CASE` (SQL Server, which has no orderable boolean).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
1
2
|
const QUERY_AGGREGATE_OPS = ['$count', '$sum', '$avg', '$min', '$max'];
|
|
2
3
|
/**
|
|
3
4
|
* Whether `op` is one of {@link QueryAggregateOp}'s known aggregate operators - validates operator
|
|
@@ -17,5 +18,5 @@ export function resolveAggregateOp(key) {
|
|
|
17
18
|
if (isQueryAggregateOp(key)) {
|
|
18
19
|
return { op: key, distinct: false };
|
|
19
20
|
}
|
|
20
|
-
throw new
|
|
21
|
+
throw new UqlUsageError(`unsupported aggregate operator: ${key}`);
|
|
21
22
|
}
|
package/dist/type/queryLock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
1
2
|
const QUERY_LOCK_WAITS = ['nowait', 'skip'];
|
|
2
3
|
function isOneOf(vals, val) {
|
|
3
4
|
return vals.includes(val);
|
|
@@ -15,7 +16,7 @@ export function parseQueryLock(lock) {
|
|
|
15
16
|
return 'block';
|
|
16
17
|
}
|
|
17
18
|
if (!isOneOf(QUERY_LOCK_WAITS, lock.$wait)) {
|
|
18
|
-
throw new
|
|
19
|
+
throw new UqlUsageError(`unknown $lock wait policy: ${String(lock.$wait)}`);
|
|
19
20
|
}
|
|
20
21
|
return lock.$wait;
|
|
21
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
|
|
2
2
|
import type { QuerySelect } from './query.js';
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
|
-
import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
|
|
4
|
+
import type { AtLeastOne, ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
|
|
5
5
|
import type { QueryVectorQuery } from './vector.js';
|
|
6
6
|
/**
|
|
7
7
|
* options for full-text-search operator.
|
|
@@ -95,12 +95,13 @@ export type QuerySizeComparisonOps = {
|
|
|
95
95
|
};
|
|
96
96
|
/**
|
|
97
97
|
* Filter by distance to a vector, `{ $near: { $vector: v, $lt: 0.35 } }`: ordered bounds only, since a
|
|
98
|
-
* distance is a float
|
|
99
|
-
*
|
|
98
|
+
* distance is a float, and at least one, since none would filter nothing. Each clause names its own
|
|
99
|
+
* `$vector`, and `$distance` falls back to the field's. `/http` input is untyped, so the dialect
|
|
100
|
+
* checks it again at run time.
|
|
100
101
|
*/
|
|
101
|
-
export type QueryVectorNear = QueryVectorQuery & {
|
|
102
|
-
[K in QueryOrderedOp]
|
|
103
|
-
}
|
|
102
|
+
export type QueryVectorNear = QueryVectorQuery & AtLeastOne<{
|
|
103
|
+
[K in QueryOrderedOp]: NonNullable<QueryWhereFieldOperatorMap<number>[K]>;
|
|
104
|
+
}>;
|
|
104
105
|
export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
|
|
105
106
|
/**
|
|
106
107
|
* whether a value is equal to the given value.
|
package/dist/type/utility.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export type RejectKeys<K> = [K] extends [never] ? unknown : Record<K & string, n
|
|
|
59
59
|
export type ExactlyOne<T> = {
|
|
60
60
|
[K in keyof T]: Readonly<Pick<T, K>> & Partial<Readonly<Record<Exclude<keyof T, K>, never>>>;
|
|
61
61
|
}[keyof T];
|
|
62
|
+
/**
|
|
63
|
+
* At least one key of `T` with its value, the rest optional: the looser sibling of {@link ExactlyOne},
|
|
64
|
+
* for options that combine but cannot all be left out. `Pick`, so every key stays linked to `T`'s own
|
|
65
|
+
* property and renames follow it through.
|
|
66
|
+
*/
|
|
67
|
+
export type AtLeastOne<T> = Partial<T> & {
|
|
68
|
+
[K in keyof T]: Pick<T, K>;
|
|
69
|
+
}[keyof T];
|
|
62
70
|
export type Unpacked<T> = T extends readonly (infer U)[] ? U : T extends (...args: unknown[]) => infer U ? U : T extends Promise<infer U> ? U : T;
|
|
63
71
|
/**
|
|
64
72
|
* Whether the value a property holds is many rather than one: a to-many relation, a scalar array, a
|
|
@@ -4,6 +4,7 @@ import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index
|
|
|
4
4
|
import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
5
5
|
import { getFieldKeys, isDatabaseWritten } from './field.util.js';
|
|
6
6
|
import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
|
|
7
|
+
import { UqlUsageError } from './uqlError.js';
|
|
7
8
|
/** The keys of `payload` a write persists as columns. */
|
|
8
9
|
export function filterFieldKeys(meta, payload, callbackKey) {
|
|
9
10
|
return getKeys(payload).filter((key) => {
|
|
@@ -194,7 +195,7 @@ export function findVectorSort(sort) {
|
|
|
194
195
|
export function vectorCandidates(q) {
|
|
195
196
|
const candidates = q.$candidates;
|
|
196
197
|
if (candidates !== undefined && (!Number.isInteger(candidates) || candidates < 1)) {
|
|
197
|
-
throw new
|
|
198
|
+
throw new UqlUsageError(`$candidates must be a positive integer, got ${JSON.stringify(candidates)}`);
|
|
198
199
|
}
|
|
199
200
|
return candidates;
|
|
200
201
|
}
|
|
@@ -260,7 +261,7 @@ export function isFieldUpdateOp(value) {
|
|
|
260
261
|
*/
|
|
261
262
|
export function fieldUpdateOf(key, value) {
|
|
262
263
|
if (value.$inc !== undefined && value.$mul !== undefined) {
|
|
263
|
-
throw new
|
|
264
|
+
throw new UqlUsageError(`'${key}' takes one of $inc and $mul`);
|
|
264
265
|
}
|
|
265
266
|
return value.$inc === undefined ? ['$mul', value.$mul] : ['$inc', value.$inc];
|
|
266
267
|
}
|
|
@@ -280,7 +281,7 @@ export function whereIds(meta, ids) {
|
|
|
280
281
|
*/
|
|
281
282
|
export function assertWhere(meta, where) {
|
|
282
283
|
if (!isWhereMap(where)) {
|
|
283
|
-
throw new
|
|
284
|
+
throw new UqlUsageError(`$where on '${entityName(meta)}' must be a map of conditions, such as { id: 1 }`);
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
/** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
|
|
@@ -353,7 +354,7 @@ export function parseRelationSize(val) {
|
|
|
353
354
|
}
|
|
354
355
|
const siblings = getKeys(val).filter((key) => key !== '$size');
|
|
355
356
|
if (siblings.length) {
|
|
356
|
-
throw new
|
|
357
|
+
throw new UqlUsageError(`$size on a relation cannot be combined with other conditions: ${siblings.join(', ')}`);
|
|
357
358
|
}
|
|
358
359
|
return val.$size;
|
|
359
360
|
}
|
|
@@ -368,7 +369,7 @@ export function parseSortByCount(val) {
|
|
|
368
369
|
}
|
|
369
370
|
const siblings = getKeys(val).filter((key) => key !== '$count');
|
|
370
371
|
if (siblings.length) {
|
|
371
|
-
throw new
|
|
372
|
+
throw new UqlUsageError(`$count in a $sort cannot be combined with other keys: ${siblings.join(', ')}`);
|
|
372
373
|
}
|
|
373
374
|
return val.$count;
|
|
374
375
|
}
|
|
@@ -393,13 +394,13 @@ export function parseGroupMap(group, select) {
|
|
|
393
394
|
const call = select[alias];
|
|
394
395
|
const key = getKeys(call).find((name) => name !== '$where');
|
|
395
396
|
if (key === undefined) {
|
|
396
|
-
throw new
|
|
397
|
+
throw new UqlUsageError(`aggregate '${alias}' names no op, only a $where`);
|
|
397
398
|
}
|
|
398
399
|
// `$countDistinct` normalizes to `$count` plus a `distinct` flag.
|
|
399
400
|
const { op, distinct } = resolveAggregateOp(key);
|
|
400
401
|
const field = aggregateField(alias, call[key]);
|
|
401
402
|
if (field === undefined && (op !== '$count' || distinct)) {
|
|
402
|
-
throw new
|
|
403
|
+
throw new UqlUsageError(`aggregate '${alias}' takes '*' only as a $count`);
|
|
403
404
|
}
|
|
404
405
|
entries.push({ kind: 'fn', alias, op, distinct, ...(field && { field }), ...(hasKeys(where) ? { where } : {}) });
|
|
405
406
|
}
|
|
@@ -409,7 +410,7 @@ export function parseGroupMap(group, select) {
|
|
|
409
410
|
function groupRefPath(alias, ref) {
|
|
410
411
|
const [key, ...rest] = isRecord(ref) ? getKeys(ref) : [];
|
|
411
412
|
if (!isRecord(ref) || key === undefined || rest.length) {
|
|
412
|
-
throw new
|
|
413
|
+
throw new UqlUsageError(`$group '${alias}' names one field by the path to it: got ${JSON.stringify(ref)}`);
|
|
413
414
|
}
|
|
414
415
|
return ref[key] === true ? [key] : [key, ...groupRefPath(alias, ref[key])];
|
|
415
416
|
}
|
|
@@ -420,7 +421,7 @@ function aggregateField(alias, arg) {
|
|
|
420
421
|
}
|
|
421
422
|
const [field, ...rest] = namedKeys(arg);
|
|
422
423
|
if (field === undefined || rest.length) {
|
|
423
|
-
throw new
|
|
424
|
+
throw new UqlUsageError(`aggregate '${alias}' takes one field as { field: true }, or '*': got ${JSON.stringify(arg)}`);
|
|
424
425
|
}
|
|
425
426
|
return field;
|
|
426
427
|
}
|
|
@@ -454,7 +455,7 @@ export function isJsonObject(value) {
|
|
|
454
455
|
*/
|
|
455
456
|
export function assertNonNegativeInteger(value, clause) {
|
|
456
457
|
if (!Number.isInteger(value) || value < 0) {
|
|
457
|
-
throw new
|
|
458
|
+
throw new UqlUsageError(`${clause} must be a non-negative integer, got ${value}`);
|
|
458
459
|
}
|
|
459
460
|
return value;
|
|
460
461
|
}
|
|
@@ -464,7 +465,7 @@ export function assertNonNegativeInteger(value, clause) {
|
|
|
464
465
|
* SQL and MongoDB refuse the same query with the same words.
|
|
465
466
|
*/
|
|
466
467
|
export function throwUnknownAggregateColumn(key, clause) {
|
|
467
|
-
throw new
|
|
468
|
+
throw new UqlUsageError(`cannot ${clause} by '${key}': it is neither a $group column nor a $select alias`);
|
|
468
469
|
}
|
|
469
470
|
/** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
|
|
470
471
|
export function assertAggregateColumns(clauseMap, emitted, clause) {
|
|
@@ -529,7 +530,7 @@ export function textSortOf(sort) {
|
|
|
529
530
|
*/
|
|
530
531
|
export function rankedTextSearch(where) {
|
|
531
532
|
if (!where?.$text) {
|
|
532
|
-
throw new
|
|
533
|
+
throw new UqlUsageError('$sort by $text ranks by the $text at the root of $where, which this query has none of');
|
|
533
534
|
}
|
|
534
535
|
return where.$text;
|
|
535
536
|
}
|
|
@@ -551,5 +552,5 @@ export function textSearchFields(meta, search) {
|
|
|
551
552
|
const declared = fulltext.length
|
|
552
553
|
? `${fulltext.length} fulltext indexes to choose from`
|
|
553
554
|
: 'no fulltext index to search';
|
|
554
|
-
throw new
|
|
555
|
+
throw new UqlUsageError(`$text on '${name}' names no $fields, and '${name}' declares ${declared}. Name them with $fields.`);
|
|
555
556
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_STATEMENT_CLAUSES, } from '../type/query.js';
|
|
2
2
|
import { getKeys, isRecord, someKey } from './object.util.js';
|
|
3
|
+
import { UqlUsageError } from './uqlError.js';
|
|
3
4
|
/** What a query populating nothing requests, shared: most reads populate nothing, and ask on every one. */
|
|
4
5
|
const NOTHING_REQUESTED = Object.freeze({
|
|
5
6
|
requestedKeys: Object.freeze([]),
|
|
@@ -65,7 +66,7 @@ function assertJoinableRelationQuery(relKey, value) {
|
|
|
65
66
|
}
|
|
66
67
|
for (const [key, reason] of JOINED_RELATION_REJECTED_KEYS) {
|
|
67
68
|
if (key in value) {
|
|
68
|
-
throw new
|
|
69
|
+
throw new UqlUsageError(`'${key}' is not supported inside $populate of the to-one relation '${relKey}': ${reason}.`);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
@@ -138,7 +139,7 @@ export function parseRelationQueryValue(value) {
|
|
|
138
139
|
if (isRecord(value)) {
|
|
139
140
|
const statementOnly = QUERY_STATEMENT_CLAUSES.find((clause) => clause in value);
|
|
140
141
|
if (statementOnly) {
|
|
141
|
-
throw new
|
|
142
|
+
throw new UqlUsageError(`'${statementOnly}' applies to the whole statement, not to a populated relation. Move it to the top level of the query.`);
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
if (isRelationQueryObject(value)) {
|
|
@@ -152,7 +153,7 @@ export function parseRelationQueryValue(value) {
|
|
|
152
153
|
return { query: { $select: selectMap }, required: false, nested: false };
|
|
153
154
|
}
|
|
154
155
|
if (value !== undefined && value !== null && value !== true && value !== 1) {
|
|
155
|
-
throw new
|
|
156
|
+
throw new UqlUsageError(`Invalid relation query value '${String(value)}'. Expected true/1, relation query object, or relation $populate array.`);
|
|
156
157
|
}
|
|
157
158
|
return { query: {}, required: false, nested: false };
|
|
158
159
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed query ran into, named the same on every engine - what {@link queryErrorKind} answers
|
|
3
|
+
* with, whether a driver raised the error or UQL did. `retryable` is a deadlock, a serialization
|
|
4
|
+
* failure, a lock timeout or a busy database: the transaction can simply run again. `usage` is the
|
|
5
|
+
* caller's own mistake, which running it again will not fix.
|
|
6
|
+
*/
|
|
7
|
+
export type QueryErrorKind = 'uniqueViolation' | 'foreignKeyViolation' | 'notNullViolation' | 'checkViolation' | 'optimisticLock' | 'retryable' | 'usage';
|
|
8
|
+
/**
|
|
9
|
+
* Thrown where the caller used the API in a way no statement can carry out: an update payload with no
|
|
10
|
+
* version, a `$lock` outside a transaction, a method with no version to match. A `TypeError` still,
|
|
11
|
+
* since the call itself is wrong, but one carrying the `status` an HTTP transport answers with - the
|
|
12
|
+
* request is malformed, not the server's failure, and an untyped client is exactly who reaches this.
|
|
13
|
+
*/
|
|
14
|
+
export declare class UqlUsageError extends TypeError {
|
|
15
|
+
name: string;
|
|
16
|
+
/** What `queryErrorKind` answers, so a caller branches on it rather than on the class. */
|
|
17
|
+
readonly kind = "usage";
|
|
18
|
+
/** What an HTTP transport answers with. */
|
|
19
|
+
readonly status = 400;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated since 0.77.1 - use {@link UqlUsageError}, which every misuse throws, lock or not. The
|
|
23
|
+
* same class under both names, so an existing `instanceof` keeps working.
|
|
24
|
+
*/
|
|
25
|
+
export declare const UqlLockUsageError: typeof UqlUsageError;
|
|
26
|
+
export type UqlLockUsageError = UqlUsageError;
|
|
27
|
+
/**
|
|
28
|
+
* Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
|
|
29
|
+
* or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
|
|
30
|
+
* where there is no row left.
|
|
31
|
+
*/
|
|
32
|
+
export declare class UqlOptimisticLockError extends Error {
|
|
33
|
+
readonly expected: unknown;
|
|
34
|
+
readonly actual: unknown;
|
|
35
|
+
name: string;
|
|
36
|
+
readonly kind = "optimisticLock";
|
|
37
|
+
readonly status = 409;
|
|
38
|
+
constructor(message: string, expected: unknown, actual: unknown);
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown where the caller used the API in a way no statement can carry out: an update payload with no
|
|
3
|
+
* version, a `$lock` outside a transaction, a method with no version to match. A `TypeError` still,
|
|
4
|
+
* since the call itself is wrong, but one carrying the `status` an HTTP transport answers with - the
|
|
5
|
+
* request is malformed, not the server's failure, and an untyped client is exactly who reaches this.
|
|
6
|
+
*/
|
|
7
|
+
export class UqlUsageError extends TypeError {
|
|
8
|
+
name = 'UqlUsageError';
|
|
9
|
+
/** What `queryErrorKind` answers, so a caller branches on it rather than on the class. */
|
|
10
|
+
kind = 'usage';
|
|
11
|
+
/** What an HTTP transport answers with. */
|
|
12
|
+
status = 400;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated since 0.77.1 - use {@link UqlUsageError}, which every misuse throws, lock or not. The
|
|
16
|
+
* same class under both names, so an existing `instanceof` keeps working.
|
|
17
|
+
*/
|
|
18
|
+
export const UqlLockUsageError = UqlUsageError;
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when an update's `@Field({ version })` no longer matches the row: another writer moved it on,
|
|
21
|
+
* or it is gone. `expected` is what the payload carried, `actual` what the row holds now, `undefined`
|
|
22
|
+
* where there is no row left.
|
|
23
|
+
*/
|
|
24
|
+
export class UqlOptimisticLockError extends Error {
|
|
25
|
+
expected;
|
|
26
|
+
actual;
|
|
27
|
+
name = 'UqlOptimisticLockError';
|
|
28
|
+
kind = 'optimisticLock';
|
|
29
|
+
status = 409;
|
|
30
|
+
constructor(message, expected, actual) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.expected = expected;
|
|
33
|
+
this.actual = actual;
|
|
34
|
+
}
|
|
35
|
+
}
|
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.77.
|
|
6
|
+
"version": "0.77.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
package/skills/uql-orm/SKILL.md
CHANGED
|
@@ -82,7 +82,7 @@ export class Post {
|
|
|
82
82
|
- `@Field({ type: Number, version: true })`, with `[versionKey]?: 'version'` on the class, makes the column an
|
|
83
83
|
optimistic lock: every update payload must carry the version it read (a compile error otherwise), the update
|
|
84
84
|
matches on it and writes the next one, and a write against a row someone else moved on throws
|
|
85
|
-
`UqlOptimisticLockError` (`
|
|
85
|
+
`UqlOptimisticLockError` (kind `optimisticLock`, HTTP 409) instead of overwriting it. Save and upsert are refused on such an entity; the update is named by its id, so `updateMany` over a many-row filter is refused too, and delete and restore carry no version.
|
|
86
86
|
- `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
|
|
87
87
|
|
|
88
88
|
## Queries
|
|
@@ -124,6 +124,12 @@ const users = await pool.findMany(User, {
|
|
|
124
124
|
- An update takes `{ stock: { $inc: -1 } }` to add, or `$mul` to multiply, in the statement, a NULL counting as 0,
|
|
125
125
|
so a guard in `$where` (`stock: { $gte: 1 }`) makes a decrement race-safe. JSON fields take `$set`, `$unset`,
|
|
126
126
|
`$push`, `$pull`.
|
|
127
|
+
- `$lock: true` locks the rows a read returns (`{ $wait: 'skip' | 'nowait' }` says what to do about a row
|
|
128
|
+
someone else holds) and needs an open transaction; SQLite, libSQL, Turso, D1 and MongoDB have no row lock and
|
|
129
|
+
refuse it.
|
|
130
|
+
- `queryErrorKind(err)` names any failure the same on every engine - `uniqueViolation`, `foreignKeyViolation`,
|
|
131
|
+
`notNullViolation`, `checkViolation`, `optimisticLock`, `retryable`, `usage` - so catch by kind rather than by
|
|
132
|
+
a driver's code or an `instanceof`.
|
|
127
133
|
- `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`.
|
|
128
134
|
|
|
129
135
|
## Connections and transactions
|