uql-orm 0.81.0 → 0.83.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 +3 -3
- package/dist/browser/querier/httpQuerier.d.ts +2 -2
- package/dist/browser/querier/httpQuerier.js +2 -1
- package/dist/browser/type/clientQuerier.d.ts +2 -2
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +9 -8
- package/dist/bunSql/bunSql.util.js +2 -1
- package/dist/cockroachdb/crdbQuerierPool.js +2 -2
- package/dist/dialect/abstractSqlDialect.d.ts +16 -6
- package/dist/dialect/abstractSqlDialect.js +110 -41
- package/dist/dialect/hydrateColumn.js +2 -12
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
- package/dist/dialect/operators.d.ts +7 -1
- package/dist/dialect/operators.js +13 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +13 -4
- package/dist/entity/metadata/definition.d.ts +1 -2
- package/dist/entity/metadata/definition.js +37 -39
- package/dist/http/handler.js +5 -4
- package/dist/http/query.d.ts +1 -1
- package/dist/http/query.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/maria/mariadbQuerierPool.js +4 -2
- package/dist/migrate/acquireQuerierForMigrations.js +2 -1
- package/dist/migrate/assertCliConfig.js +7 -6
- package/dist/migrate/bin.js +0 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +20 -10
- package/dist/migrate/builder/tableBuilder.js +1 -1
- package/dist/migrate/cli-config.js +5 -4
- package/dist/migrate/ddl/indexDdl.js +4 -3
- package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
- package/dist/migrate/ddl/pgIndexDdl.js +2 -1
- package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
- package/dist/migrate/ddl/tableDdl.js +2 -1
- package/dist/migrate/generator/mongoCommand.js +2 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
- package/dist/migrate/indexPredicate.js +2 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +3 -2
- package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/migrationTarget.js +2 -1
- package/dist/migrate/migrator.js +2 -1
- package/dist/migrate/schemaGenerator.js +5 -4
- package/dist/migrate/storage/databaseStorage.js +1 -1
- package/dist/migrate/triggerSql.d.ts +1 -1
- package/dist/migrate/triggerSql.js +77 -61
- package/dist/mongo/mongoDialect.d.ts +1 -3
- package/dist/mongo/mongoDialect.js +9 -14
- package/dist/mongo/mongodbQuerier.js +7 -10
- package/dist/mssql/mssqlQuerier.d.ts +2 -0
- package/dist/mssql/mssqlQuerier.js +8 -5
- package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
- package/dist/mysql/mysql2QuerierPool.js +20 -2
- package/dist/neon/neonQuerierPool.js +2 -2
- package/dist/pglite/pgliteQuerierPool.js +10 -4
- package/dist/postgres/pgQuerierPool.js +2 -2
- package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
- package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
- package/dist/querier/abstractQuerier.d.ts +9 -4
- package/dist/querier/abstractQuerier.js +26 -19
- package/dist/querier/abstractQuerierPool.d.ts +3 -3
- package/dist/querier/abstractSqlQuerier.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.js +1 -1
- package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
- package/dist/querier/queryError.d.ts +2 -2
- package/dist/schema/canonicalType.d.ts +3 -0
- package/dist/schema/canonicalType.js +31 -9
- package/dist/schema/schemaASTBuilder.js +2 -1
- package/dist/schema/schemaASTDiffer.js +4 -2
- package/dist/sqlite/sqliteDialect.d.ts +1 -3
- package/dist/sqlite/sqliteDialect.js +3 -6
- package/dist/type/dialect.d.ts +23 -1
- package/dist/type/entity.d.ts +16 -12
- package/dist/type/logger.d.ts +2 -2
- package/dist/type/querier.d.ts +3 -3
- package/dist/type/query.d.ts +3 -13
- package/dist/type/queryAggregate.d.ts +4 -10
- package/dist/type/queryRaw.d.ts +17 -3
- package/dist/type/queryRaw.js +2 -1
- package/dist/type/queryWhere.d.ts +7 -7
- package/dist/type/universalQuerier.d.ts +3 -3
- package/dist/type/vector.d.ts +2 -1
- package/dist/type/vector.js +2 -1
- package/dist/util/date.d.ts +11 -0
- package/dist/util/date.js +19 -0
- package/dist/util/dialect.util.d.ts +13 -5
- package/dist/util/dialect.util.js +28 -20
- package/dist/util/field.util.d.ts +4 -4
- package/dist/util/field.util.js +10 -2
- package/dist/util/fieldOption.util.d.ts +5 -3
- package/dist/util/fieldOption.util.js +6 -5
- package/dist/util/hook.util.d.ts +1 -1
- package/dist/util/hook.util.js +8 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/logger.d.ts +3 -3
- package/dist/util/object.util.js +3 -2
- package/dist/util/raw.d.ts +6 -7
- package/dist/util/raw.js +10 -12
- package/dist/util/sqlLiteral.d.ts +8 -1
- package/dist/util/sqlLiteral.js +14 -9
- package/dist/util/triggerWrite.d.ts +15 -0
- package/dist/util/triggerWrite.js +20 -0
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +4 -4
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `YYYY-MM-DD HH:mm:ss.SSS` in UTC, then `zone`: how a date is written, whichever machine writes it. Not
|
|
3
|
+
* `toISOString` as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
|
|
4
|
+
*/
|
|
5
|
+
export function utcTimestamp(date, zone = '') {
|
|
6
|
+
return date.toISOString().replace('T', ' ').replace('Z', zone);
|
|
7
|
+
}
|
|
8
|
+
/** A time of day that names no zone, which `Date` would read in the process's own. */
|
|
9
|
+
const ZONELESS_TIME = /T[\d:.]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* A timestamp's text as the `Date` it names, the one rule every driver and hydration read dates by: UTC
|
|
12
|
+
* where it names no zone, its own offset where it does, a bare day at UTC midnight, and the fraction cut
|
|
13
|
+
* to the milliseconds a `Date` holds. Text that is none of these, such as `infinity`, stays text.
|
|
14
|
+
*/
|
|
15
|
+
export function decodeDate(text) {
|
|
16
|
+
const iso = text.replace(' ', 'T').replace(/(\.\d{3})\d+/, '$1');
|
|
17
|
+
const date = new Date(ZONELESS_TIME.test(iso) ? `${iso}Z` : iso);
|
|
18
|
+
return Number.isNaN(date.getTime()) ? text : date;
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IndexType } from '../schema/types.js';
|
|
2
|
-
import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
|
|
2
|
+
import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryConflictPaths, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type QueryWhereArray, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
|
|
3
3
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
4
4
|
/** The keys of `payload` a write persists as columns. */
|
|
5
5
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
@@ -50,10 +50,18 @@ export declare function isCascadable(action: CascadeType, configuration?: boolea
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function isPagedQuery<E>(q: QuerySearch<E>): boolean;
|
|
52
52
|
/**
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*/
|
|
53
|
+
* Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
|
|
54
|
+
* below hold the casts a statement built in generic code needs: a key read at run time is no key of
|
|
55
|
+
* these maps to the compiler, whose values it works out per entity.
|
|
56
|
+
*/
|
|
57
|
+
export declare function keySet<E>(keys: readonly FieldKey<E>[]): QueryConflictPaths<E>;
|
|
58
|
+
/** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
|
|
59
|
+
export declare function whereWith<E>(key: FieldKey<E>, value: unknown, where?: QueryWhere<E>): QueryWhere<E>;
|
|
60
|
+
/** The `$where` holding each of `keys` to what `valueOf` reads for it. */
|
|
61
|
+
export declare function whereEach<E>(keys: readonly FieldKey<E>[], valueOf: (key: FieldKey<E>) => unknown): QueryWhere<E>;
|
|
62
|
+
/** The `$where` any one of `clauses` satisfies. */
|
|
63
|
+
export declare function whereAnyOf<E>(clauses: QueryWhereArray<E>): QueryWhere<E>;
|
|
64
|
+
/** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
|
|
57
65
|
export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
|
|
58
66
|
/**
|
|
59
67
|
* The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
|
|
@@ -2,7 +2,7 @@ import { getContext, UqlSecurityError } from '../context/context.js';
|
|
|
2
2
|
import { soleIdOf } from '../entity/metadata/definition.js';
|
|
3
3
|
import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
|
|
4
4
|
import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
5
|
-
import {
|
|
5
|
+
import { defaultReadKeys, fieldKeys, isDatabaseWritten } from './field.util.js';
|
|
6
6
|
import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
|
|
7
7
|
import { UqlUsageError } from './uqlError.js';
|
|
8
8
|
/** The keys of `payload` a write persists as columns. */
|
|
@@ -50,12 +50,7 @@ export function getInsertFieldKeys(meta, payloads) {
|
|
|
50
50
|
for (const record of payloads) {
|
|
51
51
|
addInsertFieldKeys(meta, record, seen, keys);
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
if (meta.fields[key].onInsert !== undefined && !seen.has(key)) {
|
|
55
|
-
keys.push(key);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return keys;
|
|
53
|
+
return [...keys, ...fieldKeys(meta, (field) => field.onInsert !== undefined).filter((key) => !seen.has(key))];
|
|
59
54
|
}
|
|
60
55
|
export function getFieldCallbackValue(val) {
|
|
61
56
|
return typeof val === 'function' ? val() : val;
|
|
@@ -72,7 +67,7 @@ export function fillOnFields(meta, payload, callbackKey) {
|
|
|
72
67
|
const payloads = Array.isArray(payload) ? payload : [payload];
|
|
73
68
|
// By presence, not truthiness, as `addInsertFieldKeys` above reads it: `onInsert: 0` and `onInsert: ''`
|
|
74
69
|
// are values a caller meant, and a falsy one was silently never filled.
|
|
75
|
-
const keys =
|
|
70
|
+
const keys = fieldKeys(meta, (field) => field[callbackKey] !== undefined);
|
|
76
71
|
if (keys.length === 0) {
|
|
77
72
|
return payloads;
|
|
78
73
|
}
|
|
@@ -120,12 +115,28 @@ export function isPagedQuery(q) {
|
|
|
120
115
|
return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
|
|
121
116
|
}
|
|
122
117
|
/**
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
125
|
-
*
|
|
118
|
+
* Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
|
|
119
|
+
* below hold the casts a statement built in generic code needs: a key read at run time is no key of
|
|
120
|
+
* these maps to the compiler, whose values it works out per entity.
|
|
126
121
|
*/
|
|
122
|
+
export function keySet(keys) {
|
|
123
|
+
return Object.fromEntries(keys.map((key) => [key, true]));
|
|
124
|
+
}
|
|
125
|
+
/** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
|
|
126
|
+
export function whereWith(key, value, where) {
|
|
127
|
+
return { ...where, [key]: value };
|
|
128
|
+
}
|
|
129
|
+
/** The `$where` holding each of `keys` to what `valueOf` reads for it. */
|
|
130
|
+
export function whereEach(keys, valueOf) {
|
|
131
|
+
return Object.fromEntries(keys.map((key) => [key, valueOf(key)]));
|
|
132
|
+
}
|
|
133
|
+
/** The `$where` any one of `clauses` satisfies. */
|
|
134
|
+
export function whereAnyOf(clauses) {
|
|
135
|
+
return { $or: clauses };
|
|
136
|
+
}
|
|
137
|
+
/** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
|
|
127
138
|
export function idOnlyQuery(meta, q) {
|
|
128
|
-
return { ...q, $select:
|
|
139
|
+
return { ...q, $select: keySet(meta.ids) };
|
|
129
140
|
}
|
|
130
141
|
/**
|
|
131
142
|
* The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
|
|
@@ -165,7 +176,7 @@ export function normalizeScalarFieldSelection(meta, select, exclude) {
|
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
}
|
|
168
|
-
const allFields =
|
|
179
|
+
const allFields = defaultReadKeys(meta);
|
|
169
180
|
if (!excludedFields) {
|
|
170
181
|
return allFields;
|
|
171
182
|
}
|
|
@@ -217,10 +228,7 @@ export function findVectorIndex(meta, key) {
|
|
|
217
228
|
* serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
|
|
218
229
|
*/
|
|
219
230
|
export function vectorDistanceOf(meta, key, search) {
|
|
220
|
-
return (search.$distance ??
|
|
221
|
-
meta.fields[key]?.distance ??
|
|
222
|
-
findVectorIndex(meta, key)?.distance ??
|
|
223
|
-
DEFAULT_VECTOR_DISTANCE);
|
|
231
|
+
return (search.$distance ?? meta.fields[key]?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE);
|
|
224
232
|
}
|
|
225
233
|
/**
|
|
226
234
|
* Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
|
|
@@ -271,7 +279,7 @@ export function fieldUpdateOf(key, value) {
|
|
|
271
279
|
*/
|
|
272
280
|
export function whereIds(meta, ids) {
|
|
273
281
|
if (Array.isArray(ids) ? ids.every(isScalarId) : isScalarId(ids)) {
|
|
274
|
-
return
|
|
282
|
+
return whereWith(soleIdOf(meta, 'addressing by a bare id value'), ids);
|
|
275
283
|
}
|
|
276
284
|
return (Array.isArray(ids) ? { $or: ids } : ids);
|
|
277
285
|
}
|
|
@@ -492,11 +500,11 @@ export function fulltextWeights(index) {
|
|
|
492
500
|
return undefined;
|
|
493
501
|
}
|
|
494
502
|
if (index.type !== 'fulltext') {
|
|
495
|
-
throw new
|
|
503
|
+
throw new UqlUsageError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
|
|
496
504
|
}
|
|
497
505
|
const weights = index.entries.map(({ weight = 1 }) => {
|
|
498
506
|
if (!Number.isInteger(weight) || weight < 1 || weight > MAX_TEXT_WEIGHT) {
|
|
499
|
-
throw new
|
|
507
|
+
throw new UqlUsageError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
|
|
500
508
|
}
|
|
501
509
|
return weight;
|
|
502
510
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
|
|
1
|
+
import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldMeta, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
|
|
2
2
|
/** The family of a logical field type, or `undefined` where it names none. */
|
|
3
3
|
export declare function columnFamily(type: unknown): ColumnFamily | undefined;
|
|
4
4
|
/**
|
|
@@ -39,11 +39,11 @@ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOption
|
|
|
39
39
|
* states its width. The one answer the create statement and the diff both read.
|
|
40
40
|
*/
|
|
41
41
|
export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
|
|
42
|
+
/** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
|
|
43
|
+
export declare function fieldKeys<E>(meta: EntityMeta<E>, pick: (field: FieldMeta) => unknown): FieldKey<E>[];
|
|
42
44
|
/**
|
|
43
45
|
* The fields a read answers with where it names none. A relation aggregate is left out unless it asks
|
|
44
46
|
* for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
|
|
45
47
|
* only when a query asks for it. Naming one in `$select` reads it, whatever the default.
|
|
46
48
|
*/
|
|
47
|
-
export declare function
|
|
48
|
-
[K in FieldKey<E>]?: FieldOptions;
|
|
49
|
-
}): FieldKey<E>[];
|
|
49
|
+
export declare function defaultReadKeys<E>(meta: EntityMeta<E>): FieldKey<E>[];
|
package/dist/util/field.util.js
CHANGED
|
@@ -83,11 +83,19 @@ export function isAutoIncrement(field, isPrimaryKey) {
|
|
|
83
83
|
return field.autoIncrement;
|
|
84
84
|
return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.references;
|
|
85
85
|
}
|
|
86
|
+
/** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
|
|
87
|
+
export function fieldKeys(meta, pick) {
|
|
88
|
+
const fields = meta.fields;
|
|
89
|
+
return getKeys(fields).filter((key) => {
|
|
90
|
+
const field = fields[key];
|
|
91
|
+
return field !== undefined && Boolean(pick(field));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
86
94
|
/**
|
|
87
95
|
* The fields a read answers with where it names none. A relation aggregate is left out unless it asks
|
|
88
96
|
* for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
|
|
89
97
|
* only when a query asks for it. Naming one in `$select` reads it, whatever the default.
|
|
90
98
|
*/
|
|
91
|
-
export function
|
|
92
|
-
return
|
|
99
|
+
export function defaultReadKeys(meta) {
|
|
100
|
+
return fieldKeys(meta, (field) => field.eager ?? !aggregateOf(field));
|
|
93
101
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ColumnFamily, type FamilyOf, type FieldOptions, QueryRaw, type StampEvent } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* The column
|
|
3
|
+
* The column families each field option means anything on, or `'*'` where it applies to every column.
|
|
4
4
|
* Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
|
|
5
5
|
* discipline `INDEX_FEATURE_LABELS` uses for index features.
|
|
6
6
|
*/
|
|
@@ -23,7 +23,7 @@ declare const FIELD_OPTION_FAMILY: {
|
|
|
23
23
|
readonly version: 'numeric';
|
|
24
24
|
readonly columnType: '*';
|
|
25
25
|
readonly length: 'string';
|
|
26
|
-
readonly precision:
|
|
26
|
+
readonly precision: readonly ["numeric", "date"];
|
|
27
27
|
readonly scale: 'numeric';
|
|
28
28
|
readonly nullable: '*';
|
|
29
29
|
readonly unique: '*';
|
|
@@ -91,8 +91,10 @@ type DeadOptions<O> = (O extends {
|
|
|
91
91
|
readonly nullable: true;
|
|
92
92
|
} ? 'nullable' : never);
|
|
93
93
|
type Given<O> = Extract<keyof O, keyof FieldOptions>;
|
|
94
|
+
/** The families option `K` applies to, `'*'` for every one. */
|
|
95
|
+
type OptionFamilies<K extends keyof FieldOptions> = (typeof FIELD_OPTION_FAMILY)[K] extends readonly (infer F)[] ? F : (typeof FIELD_OPTION_FAMILY)[K];
|
|
94
96
|
type Offending<O> = {
|
|
95
|
-
[K in Given<O>]:
|
|
97
|
+
[K in Given<O>]: [Extract<OptionFamilies<K>, OptionsFamily<O> | '*'>] extends [never] ? K : K extends DeadOptions<O> ? K : never;
|
|
96
98
|
}[Given<O>];
|
|
97
99
|
/**
|
|
98
100
|
* Every option `O` states but cannot use, mapped to `never`, so one that would be ignored does not compile.
|
|
@@ -3,7 +3,7 @@ import { columnFamily, isInlinedExpression } from './field.util.js';
|
|
|
3
3
|
import { getKeys } from './object.util.js';
|
|
4
4
|
import { constantSql } from './raw.js';
|
|
5
5
|
/**
|
|
6
|
-
* The column
|
|
6
|
+
* The column families each field option means anything on, or `'*'` where it applies to every column.
|
|
7
7
|
* Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
|
|
8
8
|
* discipline `INDEX_FEATURE_LABELS` uses for index features.
|
|
9
9
|
*/
|
|
@@ -26,7 +26,8 @@ const FIELD_OPTION_FAMILY = {
|
|
|
26
26
|
version: 'numeric',
|
|
27
27
|
columnType: '*',
|
|
28
28
|
length: 'string',
|
|
29
|
-
|
|
29
|
+
// A decimal's digits, or a timestamp's fractional-second digits.
|
|
30
|
+
precision: ['numeric', 'date'],
|
|
30
31
|
scale: 'numeric',
|
|
31
32
|
nullable: '*',
|
|
32
33
|
unique: '*',
|
|
@@ -116,11 +117,11 @@ export function fieldOptionConflict(opts) {
|
|
|
116
117
|
// conflicts always reports the same one. An option no rule knows is a typo, which `@Field`'s own check
|
|
117
118
|
// reports where it can still be spelled right.
|
|
118
119
|
for (const key of getKeys(FIELD_OPTION_FAMILY)) {
|
|
119
|
-
const applies = FIELD_OPTION_FAMILY[key];
|
|
120
|
+
const applies = [FIELD_OPTION_FAMILY[key]].flat();
|
|
120
121
|
if (opts[key] === undefined)
|
|
121
122
|
continue;
|
|
122
|
-
if (family && applies
|
|
123
|
-
return `cannot use '${key}': it applies to a ${applies} column, not to a ${family} one`;
|
|
123
|
+
if (family && !applies.includes('*') && !applies.includes(family)) {
|
|
124
|
+
return `cannot use '${key}': it applies to a ${applies.join(' or ')} column, not to a ${family} one`;
|
|
124
125
|
}
|
|
125
126
|
const dead = deadOn(opts, key);
|
|
126
127
|
if (dead) {
|
package/dist/util/hook.util.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export type HookContext = {
|
|
|
11
11
|
* Hooks are invoked with `this` bound to the payload via `call`,
|
|
12
12
|
* so mutations go directly to the original object.
|
|
13
13
|
*/
|
|
14
|
-
export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: E[], ctx: HookContext): Promise<void>;
|
|
14
|
+
export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: readonly E[], ctx: HookContext): Promise<void>;
|
package/dist/util/hook.util.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { UqlUsageError } from './uqlError.js';
|
|
2
3
|
/**
|
|
3
4
|
* Run all registered hooks for the given event on each payload.
|
|
4
5
|
* Hooks are invoked with `this` bound to the payload via `call`,
|
|
@@ -9,9 +10,15 @@ export async function runHooks(entity, event, payloads, ctx) {
|
|
|
9
10
|
const registrations = meta.hooks?.[event];
|
|
10
11
|
if (!registrations?.length)
|
|
11
12
|
return;
|
|
13
|
+
// A prototype is typed `any`, so what is read off it is typed here, where it enters.
|
|
14
|
+
const prototype = entity.prototype;
|
|
12
15
|
for (const payload of payloads) {
|
|
13
16
|
for (const { methodName } of registrations) {
|
|
14
|
-
const
|
|
17
|
+
const method = prototype[methodName];
|
|
18
|
+
if (typeof method !== 'function') {
|
|
19
|
+
throw new UqlUsageError(`'${entity.name}' runs '${methodName}' on ${event}, but has no such method`);
|
|
20
|
+
}
|
|
21
|
+
const result = method.call(payload, ctx);
|
|
15
22
|
if (result instanceof Promise)
|
|
16
23
|
await result;
|
|
17
24
|
}
|
package/dist/util/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './ddlExpression.util.js';
|
|
|
7
7
|
export * from './logger.js';
|
|
8
8
|
export * from './object.util.js';
|
|
9
9
|
export * from './raw.js';
|
|
10
|
+
export * from './triggerWrite.js';
|
|
10
11
|
export * from './rowKey.util.js';
|
|
11
12
|
export * from './relationQuery.util.js';
|
|
12
13
|
export * from './sql.util.js';
|
package/dist/util/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './ddlExpression.util.js';
|
|
|
7
7
|
export * from './logger.js';
|
|
8
8
|
export * from './object.util.js';
|
|
9
9
|
export * from './raw.js';
|
|
10
|
+
export * from './triggerWrite.js';
|
|
10
11
|
export * from './rowKey.util.js';
|
|
11
12
|
export * from './relationQuery.util.js';
|
|
12
13
|
export * from './sql.util.js';
|
package/dist/util/logger.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { Logger, LoggingOptions } from '../type/logger.js';
|
|
|
3
3
|
* Default implementation of the Logger interface using console methods.
|
|
4
4
|
*/
|
|
5
5
|
export declare class DefaultLogger implements Logger {
|
|
6
|
-
logQuery(query: string, values?: unknown[], duration?: number): void;
|
|
7
|
-
logSlowQuery(query: string, values?: unknown[], duration?: number): void;
|
|
6
|
+
logQuery(query: string, values?: readonly unknown[], duration?: number): void;
|
|
7
|
+
logSlowQuery(query: string, values?: readonly unknown[], duration?: number): void;
|
|
8
8
|
logWarn(message: string): void;
|
|
9
9
|
logError(message: string, error?: unknown): void;
|
|
10
10
|
logInfo(message: string): void;
|
|
@@ -37,7 +37,7 @@ export declare class LoggerWrapper implements Logger {
|
|
|
37
37
|
constructor(options?: LoggingOptions, config?: LoggerWrapperConfig);
|
|
38
38
|
/** Whether `logQuery` would ever actually surface bound values, given the configured levels/slowQuery/logValues. */
|
|
39
39
|
willLogValues(): boolean;
|
|
40
|
-
logQuery(query: string, values?: unknown[], duration?: number): void;
|
|
40
|
+
logQuery(query: string, values?: readonly unknown[], duration?: number): void;
|
|
41
41
|
logWarn(message: string): void;
|
|
42
42
|
logError(message: string, error?: unknown): void;
|
|
43
43
|
logInfo(message: string): void;
|
package/dist/util/object.util.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { UqlUsageError } from './uqlError.js';
|
|
1
2
|
export function throwPendingTransaction() {
|
|
2
|
-
throw
|
|
3
|
+
throw new UqlUsageError('pending transaction');
|
|
3
4
|
}
|
|
4
5
|
export function throwNoPendingTransaction() {
|
|
5
|
-
throw
|
|
6
|
+
throw new UqlUsageError('not a pending transaction');
|
|
6
7
|
}
|
|
7
8
|
export function clone(value) {
|
|
8
9
|
if (typeof value !== 'object' || value === null) {
|
package/dist/util/raw.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type ComputedRefs, type RefMap, type TriggerRowName, type Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Raw SQL, where an interpolated value binds, a `refs` field renders its column, and a `raw` renders
|
|
4
4
|
* in place: `raw`GREATEST(0, ${user.credits} - ${amount})``. A callback writes whatever it writes, so
|
|
@@ -27,10 +27,9 @@ export declare function entitySql<E>(sql: EntitySql<E>): QueryRaw;
|
|
|
27
27
|
/** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
|
|
28
28
|
export declare function entityWhere<E>(where: EntityWhere<E>): EntityWhereMeta<E>;
|
|
29
29
|
/**
|
|
30
|
-
* The fields of `
|
|
31
|
-
* against the incoming row, `OLD."col"` against the outgoing one.
|
|
32
|
-
*
|
|
30
|
+
* The fields of `entity` as the row a trigger body reads them off, qualified by the side it names:
|
|
31
|
+
* `NEW."col"` against the incoming row, `OLD."col"` against the outgoing one. Bound to the entity, as
|
|
32
|
+
* {@link refs} are, so each names its own column wherever it renders, a write to another table included.
|
|
33
|
+
* Columns only: a relation's aggregate is a subquery, and a trigger fires on one row, not over a table.
|
|
33
34
|
*/
|
|
34
|
-
export declare function rowRefs<E>(qualifier: TriggerRowName): RefMap<E>;
|
|
35
|
-
/** One field of a trigger's row, for code that names it by its key rather than off {@link rowRefs}. */
|
|
36
|
-
export declare function rowColumn(qualifier: TriggerRowName, key: string): ColumnRef;
|
|
35
|
+
export declare function rowRefs<E>(entity: Type<E>, qualifier: TriggerRowName): RefMap<E>;
|
package/dist/util/raw.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getMeta } from '../entity/metadata/definition.js';
|
|
|
2
2
|
import { ColumnRef, QueryRaw, RAW_TEXT, RelationAggregate, } from '../type/index.js';
|
|
3
3
|
import { aggregateOf, isInlinedExpression } from './field.util.js';
|
|
4
4
|
import { entityName, hasKeys } from './object.util.js';
|
|
5
|
+
import { UqlUsageError } from './uqlError.js';
|
|
5
6
|
export function raw(value, ...rest) {
|
|
6
7
|
if (!isTemplateStrings(value)) {
|
|
7
8
|
return new QueryRaw(value);
|
|
@@ -78,22 +79,19 @@ function rowsOf(q) {
|
|
|
78
79
|
function relationAggregate(spec) {
|
|
79
80
|
return new RelationAggregate(spec, (opts) => {
|
|
80
81
|
if (!opts.entity) {
|
|
81
|
-
throw new
|
|
82
|
+
throw new UqlUsageError(`'${spec.relation}' was read off a definition's refs, so it renders only inside its entity's SQL`);
|
|
82
83
|
}
|
|
83
84
|
opts.dialect.appendRelationAggregate(opts.ctx, opts.entity, spec, opts.prefix);
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
87
|
-
* The fields of `
|
|
88
|
-
* against the incoming row, `OLD."col"` against the outgoing one.
|
|
89
|
-
*
|
|
88
|
+
* The fields of `entity` as the row a trigger body reads them off, qualified by the side it names:
|
|
89
|
+
* `NEW."col"` against the incoming row, `OLD."col"` against the outgoing one. Bound to the entity, as
|
|
90
|
+
* {@link refs} are, so each names its own column wherever it renders, a write to another table included.
|
|
91
|
+
* Columns only: a relation's aggregate is a subquery, and a trigger fires on one row, not over a table.
|
|
90
92
|
*/
|
|
91
|
-
export function rowRefs(qualifier) {
|
|
92
|
-
return new Proxy({}, { get: (_, key) =>
|
|
93
|
-
}
|
|
94
|
-
/** One field of a trigger's row, for code that names it by its key rather than off {@link rowRefs}. */
|
|
95
|
-
export function rowColumn(qualifier, key) {
|
|
96
|
-
return columnRef(undefined, key, qualifier);
|
|
93
|
+
export function rowRefs(entity, qualifier) {
|
|
94
|
+
return new Proxy({}, { get: (_, key) => columnRef(entity, String(key), qualifier) });
|
|
97
95
|
}
|
|
98
96
|
/**
|
|
99
97
|
* One field as SQL, against its own entity or, read off a definition, the entity rendering it. A
|
|
@@ -103,7 +101,7 @@ function columnRef(entity, key, qualifier) {
|
|
|
103
101
|
return new ColumnRef(key, (opts) => {
|
|
104
102
|
const owner = entity ?? opts.entity;
|
|
105
103
|
if (!owner) {
|
|
106
|
-
throw new
|
|
104
|
+
throw new UqlUsageError(`'${key}' was read off a definition's refs, so it renders only inside its entity's SQL`);
|
|
107
105
|
}
|
|
108
106
|
renderColumn(getMeta(owner), key, { ...opts, entity: owner }, qualifier);
|
|
109
107
|
});
|
|
@@ -119,7 +117,7 @@ function renderColumn(meta, key, opts, qualifier) {
|
|
|
119
117
|
if (field && isInlinedExpression(field)) {
|
|
120
118
|
// A relation aggregate is a subquery correlated to a table in scope, and a trigger's row is not one.
|
|
121
119
|
if (qualifier !== undefined && aggregateOf(field)) {
|
|
122
|
-
throw new
|
|
120
|
+
throw new UqlUsageError(`'${entityName(meta)}.${key}' reads a relation, which a trigger's row cannot: it fires on one row, ` +
|
|
123
121
|
'with no table in scope to correlate a subquery to. Name the columns it is derived from instead.');
|
|
124
122
|
}
|
|
125
123
|
scope.ctx.append('(');
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export declare function escapeSingleQuotes(val: string): string;
|
|
3
3
|
/** The text a MySQL string literal's body stands for: its backslash escapes and doubled quotes undone. */
|
|
4
4
|
export declare function unescapeMysqlString(body: string): string;
|
|
5
|
-
/**
|
|
5
|
+
/** How a Postgres timestamp says it is UTC. */
|
|
6
|
+
export declare const PG_UTC = "+00";
|
|
7
|
+
/** Escape `value` for SQLite and SQL Server (single-quote doubling). */
|
|
6
8
|
export declare const escapeAnsiSqlLiteral: (value: unknown) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
|
|
11
|
+
* in the session's zone.
|
|
12
|
+
*/
|
|
13
|
+
export declare const escapePgSqlLiteral: (value: unknown) => string;
|
|
7
14
|
/** Escape `value` for MySQL and MariaDB (backslash escaping). */
|
|
8
15
|
export declare const escapeMysqlSqlLiteral: (value: unknown) => string;
|
package/dist/util/sqlLiteral.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// SQL literal escaping for `Dialect.escape`: ANSI quote doubling, or MySQL's backslashes. UQL binds
|
|
2
2
|
// values instead, so this is the hand-written-SQL hatch, and inline MySQL literals break under
|
|
3
3
|
// `NO_BACKSLASH_ESCAPES` or a GBK-like charset: prefer bound parameters. Postgres arrays are separate.
|
|
4
|
+
import { utcTimestamp } from './date.js';
|
|
5
|
+
import { UqlUsageError } from './uqlError.js';
|
|
4
6
|
const SINGLE_QUOTE = /'/g;
|
|
5
7
|
/** Doubles every single quote in `val`, the ANSI escaping shared by string literals and JSON path keys. */
|
|
6
8
|
export function escapeSingleQuotes(val) {
|
|
@@ -42,12 +44,8 @@ function bytesToHexLiteral(bytes) {
|
|
|
42
44
|
* A factory, not a function taking `escapeString` as an argument: threading it through every call
|
|
43
45
|
* measured 1.1-1.5x slower. Rejects unsupported types rather than stringifying them into SQL.
|
|
44
46
|
*/
|
|
45
|
-
function createEscaper(escapeString) {
|
|
46
|
-
|
|
47
|
-
* `YYYY-MM-DD HH:mm:ss.SSS` in UTC, so the SQL is the same whichever machine wrote it. Not `toISOString`
|
|
48
|
-
* as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
|
|
49
|
-
*/
|
|
50
|
-
const dateLiteral = (date) => escapeString(date.toISOString().replace('T', ' ').replace('Z', ''));
|
|
47
|
+
function createEscaper(escapeString, zone = '') {
|
|
48
|
+
const dateLiteral = (date) => escapeString(utcTimestamp(date, zone));
|
|
51
49
|
const sqlList = (arr) => {
|
|
52
50
|
let sql = '';
|
|
53
51
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -73,7 +71,7 @@ function createEscaper(escapeString) {
|
|
|
73
71
|
if ('toSqlString' in value && typeof value.toSqlString === 'function') {
|
|
74
72
|
return String(value.toSqlString());
|
|
75
73
|
}
|
|
76
|
-
throw new
|
|
74
|
+
throw new UqlUsageError('escapeSqlLiteral: plain objects are not supported; use bound parameters or JSON.stringify + a string column.');
|
|
77
75
|
};
|
|
78
76
|
const escapeValue = (value) => {
|
|
79
77
|
if (value === undefined || value === null) {
|
|
@@ -92,12 +90,19 @@ function createEscaper(escapeString) {
|
|
|
92
90
|
return escapeObject(value);
|
|
93
91
|
default:
|
|
94
92
|
// A symbol or a function, or a future JS type: none of them may silently become SQL.
|
|
95
|
-
throw new
|
|
93
|
+
throw new UqlUsageError(`escapeSqlLiteral: unsupported value type '${typeof value}'; use bound parameters.`);
|
|
96
94
|
}
|
|
97
95
|
};
|
|
98
96
|
return escapeValue;
|
|
99
97
|
}
|
|
100
|
-
/**
|
|
98
|
+
/** How a Postgres timestamp says it is UTC. */
|
|
99
|
+
export const PG_UTC = '+00';
|
|
100
|
+
/** Escape `value` for SQLite and SQL Server (single-quote doubling). */
|
|
101
101
|
export const escapeAnsiSqlLiteral = createEscaper(ansiStringLiteral);
|
|
102
|
+
/**
|
|
103
|
+
* Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
|
|
104
|
+
* in the session's zone.
|
|
105
|
+
*/
|
|
106
|
+
export const escapePgSqlLiteral = createEscaper(ansiStringLiteral, PG_UTC);
|
|
102
107
|
/** Escape `value` for MySQL and MariaDB (backslash escaping). */
|
|
103
108
|
export const escapeMysqlSqlLiteral = createEscaper(mysqlStringLiteral);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EntityPredicate, QueryRaw, Type, UpdatePayload, WritableKey, WriteRow } from '../type/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
|
|
4
|
+
* Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
|
|
5
|
+
* set-based trigger included, where it inserts one row for each the statement touched.
|
|
6
|
+
*/
|
|
7
|
+
export declare function insertInto<E extends object>(entity: Type<E>, row: WriteRow<E>): QueryRaw;
|
|
8
|
+
/** The rows `q.$where` names updated by a trigger's body: `updateTable(Post, { $where: { id: newRow.postId } }, set)`. */
|
|
9
|
+
export declare function updateTable<E extends object>(entity: Type<E>, q: {
|
|
10
|
+
readonly $where: EntityPredicate<E>;
|
|
11
|
+
}, set: UpdatePayload<E, QueryRaw, WritableKey<E>, never>): QueryRaw;
|
|
12
|
+
/** The rows `q.$where` names deleted by a trigger's body, outright: a soft delete is an `updateTable`. */
|
|
13
|
+
export declare function deleteFrom<E extends object>(entity: Type<E>, q: {
|
|
14
|
+
readonly $where: EntityPredicate<E>;
|
|
15
|
+
}): QueryRaw;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { raw } from './raw.js';
|
|
2
|
+
/**
|
|
3
|
+
* A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
|
|
4
|
+
* Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
|
|
5
|
+
* set-based trigger included, where it inserts one row for each the statement touched.
|
|
6
|
+
*/
|
|
7
|
+
export function insertInto(entity, row) {
|
|
8
|
+
return written({ kind: 'insert', entity, row });
|
|
9
|
+
}
|
|
10
|
+
/** The rows `q.$where` names updated by a trigger's body: `updateTable(Post, { $where: { id: newRow.postId } }, set)`. */
|
|
11
|
+
export function updateTable(entity, q, set) {
|
|
12
|
+
return written({ kind: 'update', entity, set, where: q.$where });
|
|
13
|
+
}
|
|
14
|
+
/** The rows `q.$where` names deleted by a trigger's body, outright: a soft delete is an `updateTable`. */
|
|
15
|
+
export function deleteFrom(entity, q) {
|
|
16
|
+
return written({ kind: 'delete', entity, where: q.$where });
|
|
17
|
+
}
|
|
18
|
+
function written(write) {
|
|
19
|
+
return raw(({ ctx, dialect, rows }) => dialect.triggerWrite(ctx, write, rows));
|
|
20
|
+
}
|
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.
|
|
6
|
+
"version": "0.83.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
package/skills/uql-orm/SKILL.md
CHANGED
|
@@ -74,7 +74,7 @@ export class Post {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
- Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date
|
|
77
|
+
- Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date` (an instant, bound and read as UTC on every engine; `TIMESTAMPTZ` on Postgres and CockroachDB, `DATETIME(3)` on MySQL and MariaDB; `precision` sets its fractional-second digits), `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
|
|
78
78
|
- A column is nullable unless it says `nullable: false`, and its property must admit `null` to match: `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
|
|
79
79
|
- An engine's own column type is a `raw` constant, ``columnType: raw`tsvector` ``, rendered verbatim and carrying its own `length`/`precision`: never a bare string.
|
|
80
80
|
- Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
|
|
@@ -84,7 +84,7 @@ export class Post {
|
|
|
84
84
|
an update must carry the version it read (a compile error otherwise), and one against a row someone else moved on throws `UqlOptimisticLockError` (kind `optimisticLock`, HTTP 409). Its updates name one row by its id; save and upsert are refused.
|
|
85
85
|
- `@Field({ computed })` is a value the database produces, on a `readonly` property: SQL over the row, ``(u) => raw`${u.first} || ' ' || ${u.last}` ``, or a relation aggregate, `(order) => order.items.count()`.
|
|
86
86
|
`stored: true` makes the SQL a generated column; `stored: ['insert', 'update']` makes it a stamp, a trigger writing it on those events whoever writes the row (``computed: raw`CURRENT_TIMESTAMP` ``), where `onUpdate` covers only uql's own writes.
|
|
87
|
-
- `@Trigger({ on: 'afterUpdate', of: (post) => [post.status], where: { $old: { status: 'draft' } }, run })` is a trigger the database fires; `where` holds a `$where` predicate per row it names, or SQL off the rows. `run`
|
|
87
|
+
- `@Trigger({ on: 'afterUpdate', of: (post) => [post.status], where: { $old: { status: 'draft' } }, run })` is a trigger the database fires (`defineEntity`'s `triggers` or `defineTrigger` without decorators); `where` holds a `$where` predicate per row it names, or SQL off the rows. `run` takes `(newRow, oldRow)`, each only where the event has it (no `oldRow` on insert, no `newRow` on delete), and returns the body: `insertInto(Audit, { postId: newRow.id })`, `updateTable(Audit, { $where: { postId: newRow.id } }, { status: newRow.status })` or `deleteFrom(Audit, { $where: { postId: oldRow.id } })`, typed by the entity written and rendered on every engine (no `onInsert`/`onUpdate` fills, so an insert names each field uql fills on insert unless its column has a `defaultValue`; `$where` reads the entity's own fields; no entity filters, security ones included, so a soft-delete entity is hard-deleted; an update or delete naming no rows is refused; no `$inc`/`$mul`/`$push` on SQL Server), several joined in one `raw`. Anything else is `raw` SQL over the refs, one body for every engine or `{ postgres, mssql, ... }` where they differ (SQL Server fires per statement, reading `inserted`/`deleted` as tables, with no `before*` and no `where`). MongoDB has none, and refuses a write to an entity declaring one.
|
|
88
88
|
- `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
|
|
89
89
|
|
|
90
90
|
## Queries
|
|
@@ -123,7 +123,7 @@ const users = await pool.findMany(User, {
|
|
|
123
123
|
- Methods: `findMany`, `findOne`, `findOneById`, `findManyAndCount`, `findManyStream`, `count`, `exists`,
|
|
124
124
|
`aggregate`, `insertOne`, `insertMany`, `updateOneById`, `updateMany`, `saveOne`, `saveMany`, `upsertOne`,
|
|
125
125
|
`upsertMany`, `deleteOneById`, `deleteMany`. Each takes the entity class first.
|
|
126
|
-
- `updateMany` and `deleteMany` naming no rows - no `$where
|
|
126
|
+
- `updateMany` and `deleteMany` naming no rows - no `$where` holding a value (an `undefined` or an empty group holds none), no `$limit` - throw; `{ unfiltered: true }` means the whole table.
|
|
127
127
|
- An update takes `{ stock: { $inc: -1 } }` to add, or `$mul` to multiply, in the statement, a NULL counting as 0,
|
|
128
128
|
so a guard in `$where` (`stock: { $gte: 1 }`) makes a decrement race-safe. JSON fields take `$set`, `$unset`,
|
|
129
129
|
`$push`, `$pull`.
|
|
@@ -133,7 +133,7 @@ const users = await pool.findMany(User, {
|
|
|
133
133
|
- `queryErrorKind(err)` names any failure the same on every engine - `uniqueViolation`, `foreignKeyViolation`,
|
|
134
134
|
`notNullViolation`, `checkViolation`, `optimisticLock`, `retryable`, `usage` - so catch by kind rather than by
|
|
135
135
|
a driver's code or an `instanceof`.
|
|
136
|
-
- `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`.
|
|
136
|
+
- `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`. A field read off `refs(Entity)` carries its type: on its own as a value it fits only a field of that type.
|
|
137
137
|
|
|
138
138
|
## Connections and transactions
|
|
139
139
|
|