uql-orm 0.60.0 → 0.62.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/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +12 -7
- package/dist/dialect/abstractSqlDialect.js +51 -50
- package/dist/dialect/mysqlLikeSqlDialect.js +1 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +8 -7
- package/dist/dialect/queryContext.d.ts +5 -6
- package/dist/dialect/queryContext.js +8 -8
- package/dist/entity/decorator/entity.d.ts +3 -3
- package/dist/entity/decorator/entity.js +2 -2
- package/dist/entity/decorator/members.d.ts +3 -2
- package/dist/entity/decorator/members.js +1 -0
- package/dist/entity/metadata/definition.js +12 -6
- package/dist/http/contract.d.ts +1 -1
- package/dist/http/contract.js +16 -4
- package/dist/migrate/builder/migrationBuilder.js +3 -5
- package/dist/migrate/builder/tableBuilder.js +2 -4
- package/dist/migrate/builder/types.d.ts +11 -3
- package/dist/migrate/codegen/indexDecoratorSource.js +3 -1
- package/dist/migrate/generator/definitionToNode.d.ts +8 -5
- package/dist/migrate/generator/definitionToNode.js +13 -4
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +6 -1
- package/dist/migrate/schemaGenerator.d.ts +5 -3
- package/dist/migrate/schemaGenerator.js +9 -2
- package/dist/mongo/mongoDialect.js +1 -1
- package/dist/mssql/mssqlDialect.js +3 -5
- package/dist/querier/queryError.d.ts +15 -12
- package/dist/querier/queryError.js +67 -8
- package/dist/schema/schemaASTBuilder.d.ts +6 -1
- package/dist/schema/schemaASTBuilder.js +17 -16
- package/dist/type/dialect.d.ts +15 -6
- package/dist/type/entity.d.ts +94 -34
- package/dist/type/migration.d.ts +9 -2
- package/dist/type/query.d.ts +3 -3
- package/dist/type/queryLock.d.ts +7 -7
- package/dist/type/queryLock.js +8 -6
- package/dist/type/queryRaw.d.ts +22 -30
- package/dist/type/queryRaw.js +3 -9
- package/dist/util/ddlExpression.util.d.ts +8 -13
- package/dist/util/ddlExpression.util.js +17 -19
- package/dist/util/dialect.util.d.ts +1 -1
- package/dist/util/dialect.util.js +3 -4
- package/dist/util/field.util.d.ts +1 -1
- package/dist/util/raw.d.ts +19 -17
- package/dist/util/raw.js +43 -17
- package/package.json +1 -1
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import type { LoggerWrapper } from '../util/logger.js';
|
|
2
2
|
/**
|
|
3
|
-
* A driver error
|
|
4
|
-
*
|
|
5
|
-
* carry sensitive data (PII, tokens, etc.) and would otherwise leak into whatever error-tracking
|
|
6
|
-
* pipeline (Sentry, console.error, ...) serializes the error, without the developer opting in.
|
|
3
|
+
* A driver error tagged by {@link enrichError}: `query` always, `values` only when the logger already
|
|
4
|
+
* surfaces them, since they can carry PII or tokens into whatever serializes the error.
|
|
7
5
|
*/
|
|
8
6
|
export interface QueryError extends Error {
|
|
9
7
|
query?: string;
|
|
10
8
|
values?: unknown[];
|
|
11
9
|
}
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
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' | 'retryable';
|
|
15
|
+
/**
|
|
16
|
+
* Names what `err` ran into on any engine, or `undefined` for anything else. Pure: the error is only
|
|
17
|
+
* read, so it works on any driver error, whether or not a querier saw it first.
|
|
18
|
+
*/
|
|
19
|
+
export declare function queryErrorKind(err: unknown): QueryErrorKind | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Tags `err` with the query it failed on and hands it back, so `throw enrichError(...)` reads as the
|
|
22
|
+
* control flow it is. `values` are attached only when `logger?.willLogValues()`: they already surface
|
|
23
|
+
* in the logs then, so this opens no new leak.
|
|
21
24
|
*/
|
|
22
25
|
export declare function enrichError(err: unknown, logger: LoggerWrapper | undefined, query: string, values?: unknown[]): unknown;
|
|
@@ -1,12 +1,71 @@
|
|
|
1
|
+
/** Postgres, CockroachDB, PGlite and Neon in `code`; Bun SQL in `errno`. */
|
|
2
|
+
const SQLSTATE_KINDS = new Map([
|
|
3
|
+
['23505', 'uniqueViolation'],
|
|
4
|
+
['23503', 'foreignKeyViolation'],
|
|
5
|
+
['23502', 'notNullViolation'],
|
|
6
|
+
['23514', 'checkViolation'],
|
|
7
|
+
['40P01', 'retryable'],
|
|
8
|
+
['40001', 'retryable'],
|
|
9
|
+
['55P03', 'retryable'],
|
|
10
|
+
]);
|
|
11
|
+
/** MySQL and MariaDB, in a numeric `errno`. */
|
|
12
|
+
const MYSQL_ERRNO_KINDS = new Map([
|
|
13
|
+
[1062, 'uniqueViolation'],
|
|
14
|
+
[1451, 'foreignKeyViolation'],
|
|
15
|
+
[1452, 'foreignKeyViolation'],
|
|
16
|
+
[1048, 'notNullViolation'],
|
|
17
|
+
[1364, 'notNullViolation'],
|
|
18
|
+
[3819, 'checkViolation'],
|
|
19
|
+
[4025, 'checkViolation'],
|
|
20
|
+
[1213, 'retryable'],
|
|
21
|
+
[1205, 'retryable'],
|
|
22
|
+
[3572, 'retryable'],
|
|
23
|
+
]);
|
|
24
|
+
/** MSSQL, in `number`; its 547 is a check conflict too when the message names a CHECK constraint. */
|
|
25
|
+
const MSSQL_NUMBER_KINDS = new Map([
|
|
26
|
+
[2627, 'uniqueViolation'],
|
|
27
|
+
[2601, 'uniqueViolation'],
|
|
28
|
+
[547, 'foreignKeyViolation'],
|
|
29
|
+
[515, 'notNullViolation'],
|
|
30
|
+
[1205, 'retryable'],
|
|
31
|
+
[1222, 'retryable'],
|
|
32
|
+
[3960, 'retryable'],
|
|
33
|
+
]);
|
|
34
|
+
const MONGO_CODE_KINDS = new Map([
|
|
35
|
+
[11000, 'uniqueViolation'],
|
|
36
|
+
[121, 'checkViolation'],
|
|
37
|
+
[112, 'retryable'],
|
|
38
|
+
]);
|
|
39
|
+
/** The SQLite family reports only a message on every driver, D1 included. */
|
|
40
|
+
const SQLITE_MESSAGE_KINDS = [
|
|
41
|
+
['UNIQUE constraint failed', 'uniqueViolation'],
|
|
42
|
+
['FOREIGN KEY constraint failed', 'foreignKeyViolation'],
|
|
43
|
+
['NOT NULL constraint failed', 'notNullViolation'],
|
|
44
|
+
['CHECK constraint failed', 'checkViolation'],
|
|
45
|
+
['database is locked', 'retryable'],
|
|
46
|
+
];
|
|
1
47
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
48
|
+
* Names what `err` ran into on any engine, or `undefined` for anything else. Pure: the error is only
|
|
49
|
+
* read, so it works on any driver error, whether or not a querier saw it first.
|
|
50
|
+
*/
|
|
51
|
+
export function queryErrorKind(err) {
|
|
52
|
+
if (typeof err !== 'object' || err === null) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const { code, errno, number, errorLabels, message } = err;
|
|
56
|
+
const text = typeof message === 'string' ? message : '';
|
|
57
|
+
return (SQLSTATE_KINDS.get(code) ??
|
|
58
|
+
SQLSTATE_KINDS.get(errno) ??
|
|
59
|
+
MYSQL_ERRNO_KINDS.get(errno) ??
|
|
60
|
+
(number === 547 && text.includes('CHECK constraint') ? 'checkViolation' : MSSQL_NUMBER_KINDS.get(number)) ??
|
|
61
|
+
MONGO_CODE_KINDS.get(code) ??
|
|
62
|
+
(Array.isArray(errorLabels) && errorLabels.includes('TransientTransactionError') ? 'retryable' : undefined) ??
|
|
63
|
+
SQLITE_MESSAGE_KINDS.find(([fragment]) => text.includes(fragment))?.[1]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Tags `err` with the query it failed on and hands it back, so `throw enrichError(...)` reads as the
|
|
67
|
+
* control flow it is. `values` are attached only when `logger?.willLogValues()`: they already surface
|
|
68
|
+
* in the logs then, so this opens no new leak.
|
|
10
69
|
*/
|
|
11
70
|
export function enrichError(err, logger, query, values) {
|
|
12
71
|
if (err instanceof Error) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
8
|
import type { EntityGetter } from '../type/entity.js';
|
|
9
|
-
import type { EntityMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
|
|
9
|
+
import type { EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
|
|
10
10
|
import type { NamingStrategy } from '../type/namingStrategy.js';
|
|
11
11
|
import { SchemaAST } from './schemaAST.js';
|
|
12
12
|
import { type CanonicalType, type ForeignKeyAction } from './types.js';
|
|
@@ -24,6 +24,11 @@ export interface BuildSchemaASTOptions {
|
|
|
24
24
|
namingStrategy?: NamingStrategy;
|
|
25
25
|
/** Default action for foreign key ON DELETE and ON UPDATE clauses */
|
|
26
26
|
defaultForeignKeyAction?: ForeignKeyAction;
|
|
27
|
+
/**
|
|
28
|
+
* The text of SQL an entity declares - a check, a stored computed column, an index expression or
|
|
29
|
+
* predicate - which only a dialect can render. `buildEntityAST` supplies it from the generator.
|
|
30
|
+
*/
|
|
31
|
+
compileDdl?: (sql: EntityWhereMeta<object>, entity: Type<object>) => string;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
8
|
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
|
-
import {
|
|
9
|
+
import { indexNameParts, renderIndexColumn } from '../util/ddlExpression.util.js';
|
|
10
10
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
11
11
|
import { isSoleIdField } from '../util/field.util.js';
|
|
12
12
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
@@ -30,6 +30,7 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
30
30
|
resolveSchema: options.resolveSchema ?? ((m) => m.schema),
|
|
31
31
|
resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
|
|
32
32
|
defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
|
|
33
|
+
compileDdl: options.compileDdl ?? refuseDdl,
|
|
33
34
|
};
|
|
34
35
|
for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
|
|
35
36
|
for (const entity of entities) {
|
|
@@ -38,6 +39,10 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
return ctx.ast;
|
|
40
41
|
}
|
|
42
|
+
/** The `compileDdl` of a build given no dialect, which has nothing to render an entity's SQL with. */
|
|
43
|
+
function refuseDdl() {
|
|
44
|
+
throw new TypeError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
|
|
45
|
+
}
|
|
41
46
|
/**
|
|
42
47
|
* Resolve the canonical type for a field, inheriting from the referenced
|
|
43
48
|
* entity's primary key when the field is a foreign-key reference
|
|
@@ -78,7 +83,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
78
83
|
const tableName = ctx.resolveTableName(meta);
|
|
79
84
|
const table = createTableNode(tableName, ctx.resolveSchema(meta));
|
|
80
85
|
const { columns, primaryKey } = table;
|
|
81
|
-
table.checks?.push(...(meta.checks ?? []));
|
|
86
|
+
table.checks?.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
|
|
82
87
|
// Add columns from fields
|
|
83
88
|
for (const [key, field] of definedEntries(meta.fields)) {
|
|
84
89
|
// An inlined expression has no column; a stored one is a column like any other.
|
|
@@ -98,7 +103,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
98
103
|
isPrimaryKey,
|
|
99
104
|
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
100
105
|
isUnique: field.unique ?? false,
|
|
101
|
-
generatedAs:
|
|
106
|
+
generatedAs: field.computed && ctx.compileDdl(field.computed, meta.entity),
|
|
102
107
|
comment: field.comment,
|
|
103
108
|
enum: field.enum,
|
|
104
109
|
table,
|
|
@@ -237,27 +242,23 @@ function resolveIncludeColumn(ctx, meta, column) {
|
|
|
237
242
|
function addCompositeIndex(ctx, table, meta, idxMeta) {
|
|
238
243
|
// An entry survives if it is an expression (nothing to resolve) or names a column that exists;
|
|
239
244
|
// an index left with none is dropped, the same as one naming only unknown columns always was.
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return entry;
|
|
245
|
+
const resolved = idxMeta.columns.flatMap((entry) => {
|
|
246
|
+
if (typeof entry.column !== 'string')
|
|
247
|
+
return [entry];
|
|
244
248
|
const field = meta.fields[entry.column];
|
|
245
249
|
const column = field && ctx.resolveColumnName(entry.column, field);
|
|
246
|
-
return column && table.columns.has(column) ? { ...entry, column } :
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
if (!entries.length)
|
|
250
|
+
return column && table.columns.has(column) ? [{ ...entry, column }] : [];
|
|
251
|
+
});
|
|
252
|
+
if (!resolved.length)
|
|
250
253
|
return;
|
|
251
|
-
// An index over expressions alone has no column names to build a default name from.
|
|
252
|
-
const named = entries.map((entry, at) => (entry.expression ? `expr${at}` : entry.column));
|
|
253
254
|
ctx.ast.addIndex({
|
|
254
|
-
name: idxMeta.name ?? derivedIndexName(table.name,
|
|
255
|
+
name: idxMeta.name ?? derivedIndexName(table.name, indexNameParts(resolved)),
|
|
255
256
|
table,
|
|
256
|
-
entries,
|
|
257
|
+
entries: resolved.map((entry) => renderIndexColumn(entry, (sql) => ctx.compileDdl(sql, meta.entity))),
|
|
257
258
|
include: idxMeta.include?.map((column) => resolveIncludeColumn(ctx, meta, column)),
|
|
258
259
|
unique: idxMeta.unique ?? false,
|
|
259
260
|
type: idxMeta.type,
|
|
260
|
-
where: idxMeta.where,
|
|
261
|
+
where: idxMeta.where && ctx.compileDdl(idxMeta.where, meta.entity),
|
|
261
262
|
distance: idxMeta.distance,
|
|
262
263
|
m: idxMeta.m,
|
|
263
264
|
efConstruction: idxMeta.efConstruction,
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UpdatePayload } from './entity.js';
|
|
1
|
+
import type { EntityMeta, UpdatePayload } from './entity.js';
|
|
2
2
|
import type { Query, QueryConflictPaths, QueryFilter, QueryOptions, QuerySearch } from './query.js';
|
|
3
3
|
import type { QueryAggMap, QueryAggregate, QueryGroupMap } from './queryAggregate.js';
|
|
4
4
|
import type { Type } from './utility.js';
|
|
@@ -46,7 +46,13 @@ export interface QueryContext {
|
|
|
46
46
|
createFragment(): QueryContext;
|
|
47
47
|
readonly sql: string;
|
|
48
48
|
readonly values: unknown[];
|
|
49
|
+
/** Whether a value is written as its literal rather than bound: DDL has no placeholder to bind into. */
|
|
50
|
+
readonly inlineValues: boolean;
|
|
49
51
|
}
|
|
52
|
+
export type QueryContextOptions = {
|
|
53
|
+
/** See {@link QueryContext.inlineValues}. */
|
|
54
|
+
readonly inlineValues?: boolean;
|
|
55
|
+
};
|
|
50
56
|
/**
|
|
51
57
|
* Capabilities of the database driver (transport layer).
|
|
52
58
|
*/
|
|
@@ -219,11 +225,10 @@ export interface QueryDialect {
|
|
|
219
225
|
*/
|
|
220
226
|
escape(val: unknown): string;
|
|
221
227
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* @param value the value to add
|
|
228
|
+
* The SQL `value` takes in `ctx`: a raw expression rendered in place, the literal where `ctx` inlines
|
|
229
|
+
* values, and otherwise a placeholder for the value bound.
|
|
225
230
|
*/
|
|
226
|
-
addValue(
|
|
231
|
+
addValue(ctx: QueryContext, value: unknown): string;
|
|
227
232
|
/**
|
|
228
233
|
* normalizes a value according to the dialect.
|
|
229
234
|
* @param value the value to normalize
|
|
@@ -232,7 +237,11 @@ export interface QueryDialect {
|
|
|
232
237
|
/**
|
|
233
238
|
* create a new query context.
|
|
234
239
|
*/
|
|
235
|
-
createContext(): QueryContext;
|
|
240
|
+
createContext(options?: QueryContextOptions): QueryContext;
|
|
241
|
+
/**
|
|
242
|
+
* The column a field of `meta` is stored in, named the way this dialect names columns.
|
|
243
|
+
*/
|
|
244
|
+
columnOf<E>(meta: EntityMeta<E>, key: string): string;
|
|
236
245
|
}
|
|
237
246
|
/**
|
|
238
247
|
* Supported SQL dialect identifiers.
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
|
|
2
2
|
import type { FilterOptions } from './query.js';
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
|
+
import type { QueryWhere } from './queryWhere.js';
|
|
4
5
|
import type { Except, IsMany, Json, Scalar, Type, Unpacked } from './utility.js';
|
|
5
6
|
import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
|
|
6
7
|
/**
|
|
@@ -299,7 +300,9 @@ export type TypeFor<V, T = NonNullable<V>> = IsJsonColumn<T> extends true ? Json
|
|
|
299
300
|
* live there behind an `@internal` tag and a "do not set this" note, which is a comment standing in
|
|
300
301
|
* for a type boundary.
|
|
301
302
|
*/
|
|
302
|
-
export type FieldMeta<V = TsTypeOf<FieldType>> = FieldOptions<V> & {
|
|
303
|
+
export type FieldMeta<V = TsTypeOf<FieldType>> = Except<FieldOptions<V>, 'computed'> & {
|
|
304
|
+
/** {@link FieldOptions.computed}, a callback resolved to the SQL it returns. */
|
|
305
|
+
readonly computed?: QueryRaw;
|
|
303
306
|
/**
|
|
304
307
|
* Set by `defineField` when the field gave `references` but no `type`, so schema generation resolves
|
|
305
308
|
* the column from the referenced primary key rather than from whatever ended up in `type`. That is
|
|
@@ -317,7 +320,7 @@ export type FieldMeta<V = TsTypeOf<FieldType>> = FieldOptions<V> & {
|
|
|
317
320
|
* and what a default is has to be that value, checked the same way the declared `type` is. `Scalar` by
|
|
318
321
|
* default, for the places that handle a field without knowing which one it is.
|
|
319
322
|
*/
|
|
320
|
-
export type FieldOptions<V = TsTypeOf<FieldType
|
|
323
|
+
export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
|
|
321
324
|
readonly name?: string;
|
|
322
325
|
readonly isId?: true;
|
|
323
326
|
readonly type?: FieldType;
|
|
@@ -365,9 +368,9 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
|
365
368
|
* expression will do. With `stored`, it becomes a real column - `GENERATED ALWAYS AS (...) STORED` -
|
|
366
369
|
* which the engine keeps up to date, so it can be indexed and read like any other.
|
|
367
370
|
*
|
|
368
|
-
* @example `@Field({ type: String, computed: raw
|
|
371
|
+
* @example `@Field({ type: String, computed: (user) => raw`${user.first} || ' ' || ${user.last}`, stored: true })`
|
|
369
372
|
*/
|
|
370
|
-
readonly computed?:
|
|
373
|
+
readonly computed?: EntitySql<E>;
|
|
371
374
|
/**
|
|
372
375
|
* Whether {@link FieldOptions.computed} is a column the database keeps, rather than an expression
|
|
373
376
|
* spliced into each statement. The dial to flip after profiling: `$select`, `$where` and `$sort`
|
|
@@ -466,9 +469,9 @@ export type TsTypeOf<T> = T extends StringConstructor ? string : T extends Numbe
|
|
|
466
469
|
* `@Field({ references: () => Company })` would silently downgrade a `uuid` key to TEXT on every
|
|
467
470
|
* column pointing at it.
|
|
468
471
|
*/
|
|
469
|
-
export type FieldOptionsFor<V> = (FieldOptions<NonNullable<V
|
|
472
|
+
export type FieldOptionsFor<V, E = unknown> = (FieldOptions<NonNullable<V>, E> & {
|
|
470
473
|
readonly type: TypeFor<V>;
|
|
471
|
-
}) | (FieldOptions<NonNullable<V
|
|
474
|
+
}) | (FieldOptions<NonNullable<V>, E> & {
|
|
472
475
|
readonly references: EntityGetter;
|
|
473
476
|
readonly type?: TypeFor<V>;
|
|
474
477
|
});
|
|
@@ -592,6 +595,48 @@ type RelationOptionsThroughOwner<E, O> = Pick<RelationOptions<E, O>, 'entity' |
|
|
|
592
595
|
export type KeyMap<E> = {
|
|
593
596
|
readonly [K in keyof E]-?: K;
|
|
594
597
|
};
|
|
598
|
+
declare const COLUMN_KEY: unique symbol;
|
|
599
|
+
/**
|
|
600
|
+
* A field of an entity as SQL, read off a {@link RefMap}: interpolated into `raw`, it renders as that
|
|
601
|
+
* field's column. A `QueryRaw` like any other fragment, branded with the field `K` it names.
|
|
602
|
+
*/
|
|
603
|
+
export type ColumnRef<K extends string = string> = QueryRaw & {
|
|
604
|
+
readonly [COLUMN_KEY]?: K;
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* The fields of `E` as {@link ColumnRef}s, for SQL that names them: `refs(User)` in a statement, the
|
|
608
|
+
* callback's parameter in a definition. Keyed over a type parameter constrained to `keyof E`, as
|
|
609
|
+
* {@link KeyMap} is over `keyof E`, which keeps each ref linked to its field for rename.
|
|
610
|
+
*/
|
|
611
|
+
export type RefMap<E, F extends keyof E = FieldKey<E>> = {
|
|
612
|
+
readonly [K in F]-?: ColumnRef<K & string>;
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* A callback reading an entity's fields off a {@link RefMap} for the SQL it returns. Declared as a
|
|
616
|
+
* method, bivariant in its refs, so one typed for its entity still fits where the entity is erased: the
|
|
617
|
+
* registry, which resolves it.
|
|
618
|
+
*/
|
|
619
|
+
export type SqlCallback<E> = {
|
|
620
|
+
sql(row: RefMap<E>): QueryRaw;
|
|
621
|
+
}['sql'];
|
|
622
|
+
/** SQL a definition writes: `raw`, or a {@link SqlCallback} for SQL that names the entity's fields. */
|
|
623
|
+
export type EntitySql<E> = QueryRaw | SqlCallback<E>;
|
|
624
|
+
/**
|
|
625
|
+
* A predicate DDL carries, over the entity's own fields: a relation, full-text search and a sub-query
|
|
626
|
+
* have nothing a `CHECK` or a partial index can hold. An intersection rather than `Except`, which would
|
|
627
|
+
* remap the keys and lose each one's link to its field.
|
|
628
|
+
*/
|
|
629
|
+
export type EntityPredicate<E> = QueryWhere<E> & {
|
|
630
|
+
readonly [K in RelationKey<E>]?: never;
|
|
631
|
+
} & {
|
|
632
|
+
readonly $text?: never;
|
|
633
|
+
readonly $exists?: never;
|
|
634
|
+
readonly $nexists?: never;
|
|
635
|
+
};
|
|
636
|
+
/** A definition's predicate: an {@link EntityPredicate}, or {@link EntitySql} for what one cannot say. */
|
|
637
|
+
export type EntityWhere<E> = EntityPredicate<E> | EntitySql<E>;
|
|
638
|
+
/** A definition's predicate as metadata keeps it, a callback resolved to its SQL, for the schema build to compile. */
|
|
639
|
+
export type EntityWhereMeta<E> = EntityPredicate<E> | QueryRaw;
|
|
595
640
|
export type RelationReferences = {
|
|
596
641
|
readonly local: string;
|
|
597
642
|
readonly foreign: string;
|
|
@@ -632,13 +677,13 @@ export type IndexTypeOptions = {
|
|
|
632
677
|
distance?: never;
|
|
633
678
|
};
|
|
634
679
|
/**
|
|
635
|
-
* One entry of an index: a column
|
|
636
|
-
* when the entry needs more than
|
|
680
|
+
* One entry of an index: a column by default, a {@link SqlCallback} to index an expression, or an object
|
|
681
|
+
* when the entry needs more than that.
|
|
637
682
|
*
|
|
638
683
|
* @example
|
|
639
684
|
* ```ts
|
|
640
685
|
* @Index((post) => [post.tenantId, { column: post.createdAt, order: 'desc' }]) // keyset pagination
|
|
641
|
-
* @Index(() => [raw`lower(
|
|
686
|
+
* @Index(() => [(post) => raw`lower(${post.email})`], { unique: true }) // case-insensitive uniqueness
|
|
642
687
|
* @Index((post) => [{ column: post.body, length: 64 }]) // MySQL needs a prefix on TEXT
|
|
643
688
|
* @Index((post) => [post.data], { type: 'gin' }) // JSONB containment
|
|
644
689
|
* ```
|
|
@@ -648,7 +693,7 @@ export type IndexTypeOptions = {
|
|
|
648
693
|
* default to the unchecked form for the migration builder's `table.index(...)`, which names raw
|
|
649
694
|
* table columns with no entity in scope.
|
|
650
695
|
*/
|
|
651
|
-
export type IndexColumnInput<C extends string = string, E = unknown> = C |
|
|
696
|
+
export type IndexColumnInput<C extends string = string, E = unknown> = C | SqlCallback<E> | IndexColumnOptions<C, E> | IndexJsonColumnOptions<C, E>;
|
|
652
697
|
/**
|
|
653
698
|
* The JSON entries, whose `path` is checked against the payload of the column the same entry names -
|
|
654
699
|
* a mapped union, one arm per JSON field, so `{ column: 'kind', jsonPath: { path: 'thema.color' } }`
|
|
@@ -663,7 +708,7 @@ export type IndexColumnInput<C extends string = string, E = unknown> = C | Query
|
|
|
663
708
|
* builder. An entity with no JSON field at all offers no arm, which is also the truth.
|
|
664
709
|
*/
|
|
665
710
|
type IndexJsonColumnOptions<C extends string, E> = unknown extends E ? IndexColumnModifiers & {
|
|
666
|
-
readonly column: C
|
|
711
|
+
readonly column: C;
|
|
667
712
|
} : {
|
|
668
713
|
[K in JsonColumnKey<E>]: IndexColumnPlainModifiers & {
|
|
669
714
|
readonly column: K;
|
|
@@ -762,9 +807,9 @@ export type IndexJsonArray = {
|
|
|
762
807
|
};
|
|
763
808
|
/** The modifiers that do not name a JSON path, and so need no entity to be checked against. */
|
|
764
809
|
type IndexColumnPlainModifiers = Except<IndexColumnModifiers, 'jsonPath' | 'jsonArray'>;
|
|
765
|
-
export type IndexColumnOptions<C extends string = string> = IndexColumnPlainModifiers & {
|
|
766
|
-
/** The column to index, or
|
|
767
|
-
readonly column: C |
|
|
810
|
+
export type IndexColumnOptions<C extends string = string, E = unknown> = IndexColumnPlainModifiers & {
|
|
811
|
+
/** The column to index, or a {@link SqlCallback} for an expression. */
|
|
812
|
+
readonly column: C | SqlCallback<E>;
|
|
768
813
|
readonly jsonPath?: never;
|
|
769
814
|
readonly jsonArray?: never;
|
|
770
815
|
};
|
|
@@ -778,18 +823,26 @@ export type IndexColumnSchema = IndexColumnModifiers & {
|
|
|
778
823
|
/** Whether {@link column} is an expression to emit as-is rather than an identifier to quote. */
|
|
779
824
|
readonly expression?: boolean;
|
|
780
825
|
};
|
|
826
|
+
/**
|
|
827
|
+
* One index entry as entity metadata keeps it: a member, or an expression left unrendered until the
|
|
828
|
+
* schema is built, where the dialect and the naming strategy resolve what it references. Rendered, it
|
|
829
|
+
* is an {@link IndexColumnSchema}.
|
|
830
|
+
*/
|
|
831
|
+
export type EntityIndexColumn = IndexColumnModifiers & {
|
|
832
|
+
readonly column: string | QueryRaw;
|
|
833
|
+
};
|
|
781
834
|
/**
|
|
782
835
|
* An index as stored in entity metadata: authored options with the columns normalized.
|
|
783
836
|
*/
|
|
784
|
-
export type EntityIndexMeta = {
|
|
837
|
+
export type EntityIndexMeta<E = object> = {
|
|
785
838
|
/** The indexed columns, in order. */
|
|
786
|
-
columns: readonly
|
|
839
|
+
columns: readonly EntityIndexColumn[];
|
|
787
840
|
/** Custom index name */
|
|
788
841
|
name?: string;
|
|
789
842
|
/** Whether index is unique; omit or `false` for a non-unique index (default). */
|
|
790
843
|
unique?: boolean;
|
|
791
|
-
/** Partial index
|
|
792
|
-
where?:
|
|
844
|
+
/** Partial index predicate, compiled when the schema is built. */
|
|
845
|
+
where?: EntityWhereMeta<E>;
|
|
793
846
|
/**
|
|
794
847
|
* Extra columns stored in the index but not part of its key, so a query reading only these is
|
|
795
848
|
* answered from the index alone. Postgres-wire only (`INCLUDE`).
|
|
@@ -826,9 +879,9 @@ export type EntityMeta<E> = {
|
|
|
826
879
|
[key: string]: RelationMeta | undefined;
|
|
827
880
|
};
|
|
828
881
|
/** Composite indexes defined via @Index decorator */
|
|
829
|
-
indexes?: EntityIndexMeta[];
|
|
830
|
-
/** `CHECK` constraints,
|
|
831
|
-
checks?:
|
|
882
|
+
indexes?: EntityIndexMeta<E>[];
|
|
883
|
+
/** `CHECK` constraints, compiled when the schema is built. */
|
|
884
|
+
checks?: EntityCheckMeta<E>[];
|
|
832
885
|
/** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
|
|
833
886
|
hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
|
|
834
887
|
/**
|
|
@@ -840,13 +893,21 @@ export type EntityMeta<E> = {
|
|
|
840
893
|
processedAt?: number;
|
|
841
894
|
};
|
|
842
895
|
/**
|
|
843
|
-
* A table-level `CHECK
|
|
844
|
-
*
|
|
896
|
+
* A table-level `CHECK`: a predicate over the entity's fields, or SQL reading them off refs. A value in
|
|
897
|
+
* either is written as its literal, since DDL has no placeholder to bind one into.
|
|
898
|
+
*
|
|
899
|
+
* @example `{ where: { balance: { $gte: 0 } } }`
|
|
900
|
+
* @example `{ where: (wallet) => raw`${wallet.spent} <= ${wallet.balance}` }`
|
|
845
901
|
*/
|
|
846
|
-
export type CheckOptions = {
|
|
902
|
+
export type CheckOptions<E = unknown> = {
|
|
847
903
|
/** Derived from the table and the constraint's position when absent. */
|
|
848
904
|
readonly name?: string;
|
|
849
|
-
readonly
|
|
905
|
+
readonly where: EntityWhere<E>;
|
|
906
|
+
};
|
|
907
|
+
/** A `CHECK` as entity metadata keeps it, its callback resolved. */
|
|
908
|
+
export type EntityCheckMeta<E = object> = {
|
|
909
|
+
readonly name?: string;
|
|
910
|
+
readonly where: EntityWhereMeta<E>;
|
|
850
911
|
};
|
|
851
912
|
/**
|
|
852
913
|
* An entity's members as the registry takes them, keyed by plain strings - what a decorator bag, an
|
|
@@ -860,7 +921,7 @@ export type EntityMembers = {
|
|
|
860
921
|
};
|
|
861
922
|
/** An entity's fields as `defineEntity` takes them, keyed like every entity map (see `QuerySelect`). */
|
|
862
923
|
type EntityFieldOptions<E, F extends keyof E = FieldKey<E>> = {
|
|
863
|
-
readonly [K in F]?: FieldOptionsFor<E[K]>;
|
|
924
|
+
readonly [K in F]?: FieldOptionsFor<E[K], E>;
|
|
864
925
|
};
|
|
865
926
|
/**
|
|
866
927
|
* An entity's relations as `defineEntity` takes them. Keyed over every member rather than `RelationKey<E>`:
|
|
@@ -897,7 +958,7 @@ export type EntityOptions<E = unknown> = {
|
|
|
897
958
|
readonly relations?: EntityRelationOptions<E>;
|
|
898
959
|
readonly indexes?: readonly EntityIndexInput<E>[];
|
|
899
960
|
/** Table-level `CHECK` constraints. See {@link CheckOptions}. */
|
|
900
|
-
readonly checks?: readonly CheckOptions[];
|
|
961
|
+
readonly checks?: readonly CheckOptions<E>[];
|
|
901
962
|
/** Each lifecycle event and the methods it runs, read off the key map: `{ beforeInsert: (post) => [post.stamp] }`. */
|
|
902
963
|
readonly hooks?: Partial<Record<HookEvent, (keys: KeyMap<E>) => readonly MethodKey<E>[]>>;
|
|
903
964
|
};
|
|
@@ -909,18 +970,17 @@ export type EntityOptions<E = unknown> = {
|
|
|
909
970
|
export type IndexOptions = Except<EntityIndexMeta, 'columns' | 'include' | 'where'> & {
|
|
910
971
|
/** Non-key columns stored in the index, by column name; a typo builds nothing, the server refusing it. */
|
|
911
972
|
readonly include?: readonly string[];
|
|
912
|
-
/**
|
|
913
|
-
|
|
914
|
-
* there is no placeholder for a bound value. A bare string is the older spelling and still works.
|
|
915
|
-
*/
|
|
916
|
-
readonly where?: string | QueryRaw;
|
|
973
|
+
/** Partial-index predicate, as `raw` with no interpolation: the migration builder has no entity to compile one against. */
|
|
974
|
+
readonly where?: QueryRaw;
|
|
917
975
|
};
|
|
918
976
|
/**
|
|
919
977
|
* {@link IndexOptions} on an entity, whose stored columns are read off its key map, `(post) => [post.slug]`,
|
|
920
978
|
* so they are checked against it and follow a rename. The migration builder names raw columns instead.
|
|
921
979
|
*/
|
|
922
|
-
export type EntityIndexOptions<E> = Except<IndexOptions, 'include'> & {
|
|
980
|
+
export type EntityIndexOptions<E> = Except<IndexOptions, 'include' | 'where'> & {
|
|
923
981
|
readonly include?: (keys: KeyMap<E>) => readonly FieldKey<E>[];
|
|
982
|
+
/** Partial-index predicate. See {@link EntityWhere}. */
|
|
983
|
+
readonly where?: EntityWhere<E>;
|
|
924
984
|
};
|
|
925
985
|
/**
|
|
926
986
|
* An index as authored on an entity, before `defineIndex` reads its columns off the key map. Only the
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { VectorCast } from '../dialect/vectorCast.js';
|
|
2
|
-
import type { FullColumnDefinition, TableDefinition } from '../migrate/builder/types.js';
|
|
2
|
+
import type { FullColumnDefinition, IndexDefinition, TableDefinition } from '../migrate/builder/types.js';
|
|
3
3
|
import type { IndexFacet } from '../schema/indexDifferences.js';
|
|
4
4
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
5
5
|
import type { ColumnNode, ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
|
|
6
|
-
import type { EntityMeta, FieldOptions, IndexColumnSchema, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
|
|
6
|
+
import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
|
|
7
7
|
/**
|
|
8
8
|
* Defines a migration using a simple object literal. `Q` is `MongoQuerier` for a MongoDB migration.
|
|
9
9
|
*/
|
|
@@ -295,6 +295,11 @@ export interface SchemaGenerator {
|
|
|
295
295
|
* Get the SQL type for a field based on its options
|
|
296
296
|
*/
|
|
297
297
|
getSqlType(fieldOptions: FieldOptions): string;
|
|
298
|
+
/**
|
|
299
|
+
* The text of SQL an entity declares - a check, a stored computed column, an index expression or
|
|
300
|
+
* predicate - rendered for this engine, which is what building an entity's schema needs from it.
|
|
301
|
+
*/
|
|
302
|
+
compileDdl(sql: EntityWhereMeta<object>, entity: Type<object>): string;
|
|
298
303
|
/**
|
|
299
304
|
* Compare an entity with a database table node and return the differences.
|
|
300
305
|
*
|
|
@@ -361,6 +366,8 @@ export interface SqlDdlGenerator extends SchemaGenerator {
|
|
|
361
366
|
generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
|
|
362
367
|
/** Generate DROP FOREIGN KEY statement */
|
|
363
368
|
generateDropForeignKeySql(tableName: string, constraintName: string): string;
|
|
369
|
+
/** CREATE INDEX from a builder's {@link IndexDefinition}, its SQL rendered for this engine. */
|
|
370
|
+
generateCreateIndexFromDefinition(tableName: string, index: IndexDefinition): string;
|
|
364
371
|
}
|
|
365
372
|
/**
|
|
366
373
|
* Interface for introspecting the current database schema
|
package/dist/type/query.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ export interface UqlContext {
|
|
|
90
90
|
* A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.
|
|
91
91
|
* Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).
|
|
92
92
|
*/
|
|
93
|
-
export type
|
|
93
|
+
export type FilterWhere<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);
|
|
94
94
|
/**
|
|
95
95
|
* What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);
|
|
96
96
|
* `throw` fails closed (the default for `security` filters).
|
|
@@ -100,7 +100,7 @@ export type FilterOnMissing = 'skip' | 'throw';
|
|
|
100
100
|
* Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.
|
|
101
101
|
*/
|
|
102
102
|
export type FilterOptions<E = unknown> = {
|
|
103
|
-
readonly
|
|
103
|
+
readonly where: FilterWhere<E>;
|
|
104
104
|
/** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */
|
|
105
105
|
readonly default?: boolean;
|
|
106
106
|
/**
|
|
@@ -108,7 +108,7 @@ export type FilterOptions<E = unknown> = {
|
|
|
108
108
|
* AND-merged so a client `$where` on the same field can't override it.
|
|
109
109
|
*/
|
|
110
110
|
readonly security?: boolean;
|
|
111
|
-
/** What to do when
|
|
111
|
+
/** What to do when {@link where} returns `undefined`. Defaults to `skip`, or `throw` for `security`. */
|
|
112
112
|
readonly onMissing?: FilterOnMissing;
|
|
113
113
|
};
|
|
114
114
|
/**
|
package/dist/type/queryLock.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
declare const QUERY_LOCK_WAITS: readonly ['
|
|
1
|
+
declare const QUERY_LOCK_WAITS: readonly ['nowait', 'skip'];
|
|
2
2
|
/**
|
|
3
|
-
* What to do about a row someone else already holds. `block` (
|
|
4
|
-
* fails the statement at once; `skip` leaves the row out of the result, which is what makes a
|
|
3
|
+
* What to do about a row someone else already holds. `block` (what `true` asks for) waits for them;
|
|
4
|
+
* `nowait` fails the statement at once; `skip` leaves the row out of the result, which is what makes a
|
|
5
5
|
* work-queue possible: each worker takes rows nobody else has.
|
|
6
6
|
*/
|
|
7
|
-
export type QueryLockWait = (typeof QUERY_LOCK_WAITS)[number];
|
|
7
|
+
export type QueryLockWait = 'block' | (typeof QUERY_LOCK_WAITS)[number];
|
|
8
8
|
/**
|
|
9
|
-
* `true` takes the lock and waits for anyone holding the rows;
|
|
10
|
-
*
|
|
9
|
+
* `true` takes the lock and waits for anyone holding the rows; `$wait` chooses what to do instead of
|
|
10
|
+
* waiting. `false` takes none, so a query built conditionally needs no branch.
|
|
11
11
|
*/
|
|
12
12
|
export type QueryLock = boolean | {
|
|
13
|
-
readonly wait
|
|
13
|
+
readonly $wait: (typeof QUERY_LOCK_WAITS)[number];
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
16
|
* The wait policy this lock resolves to, or `undefined` when there is no lock. An unknown policy
|
package/dist/type/queryLock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const QUERY_LOCK_WAITS = ['
|
|
1
|
+
const QUERY_LOCK_WAITS = ['nowait', 'skip'];
|
|
2
2
|
function isOneOf(vals, val) {
|
|
3
3
|
return vals.includes(val);
|
|
4
4
|
}
|
|
@@ -8,12 +8,14 @@ function isOneOf(vals, val) {
|
|
|
8
8
|
* the SQL it would have produced.
|
|
9
9
|
*/
|
|
10
10
|
export function parseQueryLock(lock) {
|
|
11
|
-
if (lock
|
|
11
|
+
if (!lock) {
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new TypeError(`unknown $lock wait policy: ${String(wait)}`);
|
|
14
|
+
if (lock === true) {
|
|
15
|
+
return 'block';
|
|
17
16
|
}
|
|
18
|
-
|
|
17
|
+
if (!isOneOf(QUERY_LOCK_WAITS, lock.$wait)) {
|
|
18
|
+
throw new TypeError(`unknown $lock wait policy: ${String(lock.$wait)}`);
|
|
19
|
+
}
|
|
20
|
+
return lock.$wait;
|
|
19
21
|
}
|