uql-orm 0.45.0 → 0.46.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 +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +9 -2
- package/dist/dialect/abstractSqlDialect.js +25 -16
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/entity/metadata/definition.js +8 -1
- package/dist/migrate/builder/columnBuilder.d.ts +14 -0
- package/dist/migrate/builder/columnBuilder.js +28 -9
- package/dist/migrate/builder/tableBuilder.js +8 -19
- package/dist/migrate/builder/types.d.ts +16 -33
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
- package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
- package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
- package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
- package/dist/migrate/codegen/sourceLiteral.js +17 -0
- package/dist/migrate/generator/definitionToNode.d.ts +21 -0
- package/dist/migrate/generator/definitionToNode.js +47 -25
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +2 -2
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
- package/dist/migrate/schemaGenerator.d.ts +48 -2
- package/dist/migrate/schemaGenerator.js +120 -38
- package/dist/mongo/mongoDialect.js +2 -1
- package/dist/schema/schemaAST.d.ts +34 -3
- package/dist/schema/schemaAST.js +4 -9
- package/dist/schema/schemaASTBuilder.js +5 -2
- package/dist/schema/schemaASTDiffer.js +8 -4
- package/dist/schema/types.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +21 -2
- package/dist/type/entity.d.ts +21 -0
- package/dist/type/migration.d.ts +11 -11
- package/dist/util/dialect.util.js +5 -4
- package/dist/util/field.util.d.ts +23 -0
- package/dist/util/field.util.js +28 -0
- package/dist/util/fieldOption.util.d.ts +16 -3
- package/dist/util/fieldOption.util.js +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The query is just JSON: build it dynamically, store it, diff it, or send it from
|
|
|
53
53
|
- **One API, everywhere it runs.** PostgreSQL, PGlite, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun's native SQL, and even MongoDB. The same code on Node 24+, Bun, Deno, [Cloudflare Workers](https://uql-orm.dev/cloudflare-d1), [AWS Lambda and Vercel](https://uql-orm.dev/serverless), and [the browser](https://uql-orm.dev/browser), with no native binaries on the `fetch`-based drivers.
|
|
54
54
|
- **Relations without N+1.** [`$populate`](https://uql-orm.dev/querying/relations) loads a to-many with one query for all parents, not one per parent. Nothing is lazy, so nothing fires behind your back in a serializer.
|
|
55
55
|
- **Migrations you read before they run.** Edit an entity, run `uql-migrate generate:entities`, review the SQL in the PR like any other file. [`drift:check`](https://uql-orm.dev/migrations) catches a database that no longer matches.
|
|
56
|
-
- **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [
|
|
56
|
+
- **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [computed fields](https://uql-orm.dev/entities/computed-fields) are expressions you can filter on, and a migration can be plain SQL.
|
|
57
57
|
- **Light.** Zero runtime dependencies, under 280 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
58
58
|
- **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
|
|
59
59
|
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), by 2.6-3x over the next closest and roughly 10x over the slowest, on Bun, Node and Deno alike. The same benchmark [scores the types](https://github.com/rogerpadilla/ts-orm-benchmark#type-safety) by writing ten ordinary mistakes in six ORMs' APIs and compiling them: UQL is the only one that catches all ten.
|
|
@@ -171,6 +171,13 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
171
171
|
* `NOT (... <=> ...)` - instead of having to fall back to a form that takes none.
|
|
172
172
|
*/
|
|
173
173
|
protected resolveOperandField<E>(ctx: QueryContext, entity: Type<E>, key: string, opts: QueryOptions): string;
|
|
174
|
+
/**
|
|
175
|
+
* The expression an inlined computed field stands for, or nothing when the field is a real column.
|
|
176
|
+
*
|
|
177
|
+
* Every clause that names such a field needs the expression itself, never the output alias: an
|
|
178
|
+
* alias exists only when the field was also selected, which `$where` and `$sort` cannot assume.
|
|
179
|
+
*/
|
|
180
|
+
private inlinedOperand;
|
|
174
181
|
compareFieldOperator<E, K extends keyof QueryWhereFieldOperatorMap<E>>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, op: K, val: QueryWhereFieldOperatorMap<E>[K], opts?: QueryOptions): void;
|
|
175
182
|
/**
|
|
176
183
|
* `<operand> <op> <value>` for every operator that needs nothing but its left-hand SQL, or
|
|
@@ -261,8 +268,8 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
261
268
|
*/
|
|
262
269
|
private collectSortTerms;
|
|
263
270
|
/**
|
|
264
|
-
* The `ORDER BY` operand for one key. A key that is not a
|
|
265
|
-
* `
|
|
271
|
+
* The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
|
|
272
|
+
* `$agg` alias - is an output alias, which is never table-qualified and needs no resolving.
|
|
266
273
|
*/
|
|
267
274
|
private sortColumn;
|
|
268
275
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
|
+
import { computedExpression, isInlinedExpression } from '../util/field.util.js';
|
|
3
4
|
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
5
|
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
5
6
|
import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
|
|
@@ -160,9 +161,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
160
161
|
const field = meta.fields[key];
|
|
161
162
|
if (!field)
|
|
162
163
|
return;
|
|
163
|
-
if (field
|
|
164
|
+
if (isInlinedExpression(field)) {
|
|
164
165
|
this.getRawValue(ctx, {
|
|
165
|
-
value: field.
|
|
166
|
+
value: computedExpression(field).as(key),
|
|
166
167
|
prefix: opts.prefix,
|
|
167
168
|
escapedPrefix,
|
|
168
169
|
autoPrefixAlias: opts.autoPrefixAlias,
|
|
@@ -485,15 +486,23 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
485
486
|
*/
|
|
486
487
|
resolveOperandField(ctx, entity, key, opts) {
|
|
487
488
|
const field = getMeta(entity).fields[key];
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
489
|
+
return this.inlinedOperand(ctx, field, opts.prefix) ?? this.columnWithPrefix(key, field, opts.prefix);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The expression an inlined computed field stands for, or nothing when the field is a real column.
|
|
493
|
+
*
|
|
494
|
+
* Every clause that names such a field needs the expression itself, never the output alias: an
|
|
495
|
+
* alias exists only when the field was also selected, which `$where` and `$sort` cannot assume.
|
|
496
|
+
*/
|
|
497
|
+
inlinedOperand(ctx, field, prefix) {
|
|
498
|
+
const inlined = field && isInlinedExpression(field) ? computedExpression(field) : undefined;
|
|
499
|
+
return inlined
|
|
500
|
+
? this.buildFragment(ctx, (fragmentCtx) => this.getRawValue(fragmentCtx, {
|
|
501
|
+
value: inlined,
|
|
502
|
+
prefix,
|
|
503
|
+
escapedPrefix: this.escapeId(prefix, true, true),
|
|
504
|
+
}))
|
|
505
|
+
: undefined;
|
|
497
506
|
}
|
|
498
507
|
compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
|
|
499
508
|
const field = this.resolveOperandField(ctx, entity, key, opts);
|
|
@@ -767,17 +776,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
767
776
|
: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorSort(fragmentCtx, meta, key, value)));
|
|
768
777
|
continue;
|
|
769
778
|
}
|
|
770
|
-
columns.push(this.sortColumn(meta, key, prefix) + this.resolveSortDirection(value));
|
|
779
|
+
columns.push(this.sortColumn(ctx, meta, key, prefix) + this.resolveSortDirection(value));
|
|
771
780
|
}
|
|
772
781
|
}
|
|
773
782
|
/**
|
|
774
|
-
* The `ORDER BY` operand for one key. A key that is not a
|
|
775
|
-
* `
|
|
783
|
+
* The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
|
|
784
|
+
* `$agg` alias - is an output alias, which is never table-qualified and needs no resolving.
|
|
776
785
|
*/
|
|
777
|
-
sortColumn(meta, key, prefix) {
|
|
786
|
+
sortColumn(ctx, meta, key, prefix) {
|
|
778
787
|
const field = meta.fields[key];
|
|
779
788
|
if (field) {
|
|
780
|
-
return
|
|
789
|
+
return this.inlinedOperand(ctx, field, prefix) ?? this.columnWithPrefix(key, field, prefix);
|
|
781
790
|
}
|
|
782
791
|
const json = this.resolveJsonDotPath(meta, key, prefix);
|
|
783
792
|
return json ? this.jsonPathExpr(json.column, json.jsonPath, 'text') : this.escapeId(key);
|
|
@@ -32,7 +32,8 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
32
32
|
renameColumn: true,
|
|
33
33
|
foreignKeyAlter: true,
|
|
34
34
|
primaryKeyAlter: true,
|
|
35
|
-
|
|
35
|
+
generatedColumnAdd: true,
|
|
36
|
+
commentSyntax: 'inline',
|
|
36
37
|
vectorIndexRequiresNotNull: false,
|
|
37
38
|
vectorSupportsLength: false,
|
|
38
39
|
supportsTimestamptz: false,
|
|
@@ -29,7 +29,8 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
29
29
|
renameColumn: true,
|
|
30
30
|
foreignKeyAlter: true,
|
|
31
31
|
primaryKeyAlter: true,
|
|
32
|
-
|
|
32
|
+
generatedColumnAdd: true,
|
|
33
|
+
commentSyntax: 'statement',
|
|
33
34
|
vectorIndexRequiresNotNull: false,
|
|
34
35
|
vectorSupportsLength: true,
|
|
35
36
|
supportsTimestamptz: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
+
import { isInlinedExpression } from '../../util/field.util.js';
|
|
2
3
|
import { entityName, fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
|
|
3
4
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
4
5
|
/**
|
|
@@ -16,7 +17,13 @@ function globalMap(key) {
|
|
|
16
17
|
const metas = globalMap('uql-orm/entity/metadata/v1');
|
|
17
18
|
export function defineField(entity, key, opts = {}) {
|
|
18
19
|
const meta = ensureWritableMeta(entity);
|
|
19
|
-
if (
|
|
20
|
+
if (opts.virtual !== undefined && opts.computed !== undefined) {
|
|
21
|
+
throw new TypeError(`'${entity.name}.${key}' gives both 'virtual' and 'computed'. They are one option under two names - ` +
|
|
22
|
+
"keep 'computed'; 'npx uql-codemod' rewrites the other.");
|
|
23
|
+
}
|
|
24
|
+
// A stored computed column is a real column and still needs a type; only an inlined one is exempt,
|
|
25
|
+
// its expression being spliced in rather than declared.
|
|
26
|
+
if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
|
|
20
27
|
throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
|
|
21
28
|
"at another entity with 'references', which resolves the column type from its primary key.");
|
|
22
29
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fluent API for defining columns in migrations.
|
|
5
5
|
*/
|
|
6
|
+
import type { EnumValues } from '../../schema/types.js';
|
|
6
7
|
import type { CanonicalType, ForeignKeyAction } from '../../schema/types.js';
|
|
7
8
|
import type { BaseColumnOptions, FullColumnDefinition, IColumnBuilder, IForeignKeyBuilder } from './types.js';
|
|
8
9
|
/**
|
|
@@ -17,6 +18,8 @@ export declare class ColumnBuilder implements IColumnBuilder, IForeignKeyBuilder
|
|
|
17
18
|
private _primaryKey;
|
|
18
19
|
private _autoIncrement;
|
|
19
20
|
private _unique;
|
|
21
|
+
private _enum?;
|
|
22
|
+
private _generatedAs?;
|
|
20
23
|
private _comment?;
|
|
21
24
|
private _index?;
|
|
22
25
|
private _foreignKey?;
|
|
@@ -45,6 +48,17 @@ export declare class ColumnBuilder implements IColumnBuilder, IForeignKeyBuilder
|
|
|
45
48
|
* Add a unique constraint.
|
|
46
49
|
*/
|
|
47
50
|
unique(): this;
|
|
51
|
+
/**
|
|
52
|
+
* Make the column one the database computes: `GENERATED ALWAYS AS (<sql>) STORED`.
|
|
53
|
+
*
|
|
54
|
+
* Takes the SQL as text, since a `CREATE TABLE` has nowhere to bind a value into - the same reason
|
|
55
|
+
* a check expression and a partial-index predicate do.
|
|
56
|
+
*/
|
|
57
|
+
computed(sql: string): this;
|
|
58
|
+
/**
|
|
59
|
+
* Constrain the column to these values, as a `CHECK (col IN (...))` - what `@Field({ enum })` emits.
|
|
60
|
+
*/
|
|
61
|
+
enum(values: EnumValues): this;
|
|
48
62
|
/**
|
|
49
63
|
* Add a comment to the column.
|
|
50
64
|
*/
|
|
@@ -15,6 +15,8 @@ export class ColumnBuilder {
|
|
|
15
15
|
_primaryKey;
|
|
16
16
|
_autoIncrement;
|
|
17
17
|
_unique;
|
|
18
|
+
_enum;
|
|
19
|
+
_generatedAs;
|
|
18
20
|
_comment;
|
|
19
21
|
_index;
|
|
20
22
|
_foreignKey;
|
|
@@ -35,8 +37,7 @@ export class ColumnBuilder {
|
|
|
35
37
|
// Handle inline references option
|
|
36
38
|
if (options.references) {
|
|
37
39
|
this._foreignKey = {
|
|
38
|
-
table: options.references.table,
|
|
39
|
-
columns: [options.references.column ?? 'id'],
|
|
40
|
+
references: { table: options.references.table, columns: [options.references.column ?? 'id'] },
|
|
40
41
|
onDelete: options.references.onDelete ?? 'NO ACTION',
|
|
41
42
|
onUpdate: options.references.onUpdate ?? 'NO ACTION',
|
|
42
43
|
};
|
|
@@ -85,6 +86,23 @@ export class ColumnBuilder {
|
|
|
85
86
|
this._unique = true;
|
|
86
87
|
return this;
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Make the column one the database computes: `GENERATED ALWAYS AS (<sql>) STORED`.
|
|
91
|
+
*
|
|
92
|
+
* Takes the SQL as text, since a `CREATE TABLE` has nowhere to bind a value into - the same reason
|
|
93
|
+
* a check expression and a partial-index predicate do.
|
|
94
|
+
*/
|
|
95
|
+
computed(sql) {
|
|
96
|
+
this._generatedAs = sql;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Constrain the column to these values, as a `CHECK (col IN (...))` - what `@Field({ enum })` emits.
|
|
101
|
+
*/
|
|
102
|
+
enum(values) {
|
|
103
|
+
this._enum = values;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
88
106
|
/**
|
|
89
107
|
* Add a comment to the column.
|
|
90
108
|
*/
|
|
@@ -113,8 +131,7 @@ export class ColumnBuilder {
|
|
|
113
131
|
*/
|
|
114
132
|
references(table, column = 'id') {
|
|
115
133
|
this._foreignKey = {
|
|
116
|
-
table,
|
|
117
|
-
columns: [column],
|
|
134
|
+
references: { table, columns: [column] },
|
|
118
135
|
onDelete: 'NO ACTION',
|
|
119
136
|
onUpdate: 'NO ACTION',
|
|
120
137
|
};
|
|
@@ -125,7 +142,7 @@ export class ColumnBuilder {
|
|
|
125
142
|
*/
|
|
126
143
|
onDelete(action) {
|
|
127
144
|
if (this._foreignKey) {
|
|
128
|
-
this._foreignKey.onDelete
|
|
145
|
+
this._foreignKey = { ...this._foreignKey, onDelete: action };
|
|
129
146
|
}
|
|
130
147
|
return this;
|
|
131
148
|
}
|
|
@@ -134,7 +151,7 @@ export class ColumnBuilder {
|
|
|
134
151
|
*/
|
|
135
152
|
onUpdate(action) {
|
|
136
153
|
if (this._foreignKey) {
|
|
137
|
-
this._foreignKey.onUpdate
|
|
154
|
+
this._foreignKey = { ...this._foreignKey, onUpdate: action };
|
|
138
155
|
}
|
|
139
156
|
return this;
|
|
140
157
|
}
|
|
@@ -147,9 +164,11 @@ export class ColumnBuilder {
|
|
|
147
164
|
type: this._type,
|
|
148
165
|
nullable: this._nullable,
|
|
149
166
|
defaultValue: this._defaultValue,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
isPrimaryKey: this._primaryKey,
|
|
168
|
+
isAutoIncrement: this._autoIncrement,
|
|
169
|
+
isUnique: this._unique,
|
|
170
|
+
enum: this._enum,
|
|
171
|
+
generatedAs: this._generatedAs,
|
|
153
172
|
comment: this._comment,
|
|
154
173
|
index: this._index,
|
|
155
174
|
foreignKey: this._foreignKey,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ddlText, normalizeIndexColumn } from '../../util/index.js';
|
|
7
7
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
8
|
+
import { columnForeignKey, columnIndex } from '../generator/definitionToNode.js';
|
|
8
9
|
import { ColumnBuilder } from './columnBuilder.js';
|
|
9
10
|
import { expr } from './expressions.js';
|
|
10
11
|
/**
|
|
@@ -193,18 +194,11 @@ export class TableBuilder {
|
|
|
193
194
|
build() {
|
|
194
195
|
// Build all columns from builders
|
|
195
196
|
const columns = this._columnBuilders.map((cb) => cb.build());
|
|
196
|
-
// Collect column-level indexes
|
|
197
|
+
// Collect column-level indexes, skipping any a table-level one already names.
|
|
197
198
|
for (const col of columns) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!this._indexes.some((idx) => idx.name === indexName)) {
|
|
202
|
-
this._indexes.push({
|
|
203
|
-
name: indexName,
|
|
204
|
-
entries: [{ column: col.name }],
|
|
205
|
-
unique: col.unique,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
199
|
+
const index = columnIndex(this._name, col);
|
|
200
|
+
if (index && !this._indexes.some((idx) => idx.name === index.name)) {
|
|
201
|
+
this._indexes.push(index);
|
|
208
202
|
}
|
|
209
203
|
}
|
|
210
204
|
// Build foreign keys
|
|
@@ -213,14 +207,9 @@ export class TableBuilder {
|
|
|
213
207
|
.filter((fk) => fk !== undefined);
|
|
214
208
|
// Collect column-level foreign keys
|
|
215
209
|
for (const col of columns) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
columns: [col.name],
|
|
220
|
-
references: { table: col.foreignKey.table, columns: col.foreignKey.columns },
|
|
221
|
-
onDelete: col.foreignKey.onDelete,
|
|
222
|
-
onUpdate: col.foreignKey.onUpdate,
|
|
223
|
-
});
|
|
210
|
+
const foreignKey = columnForeignKey(col);
|
|
211
|
+
if (foreignKey) {
|
|
212
|
+
foreignKeys.push(foreignKey);
|
|
224
213
|
}
|
|
225
214
|
}
|
|
226
215
|
return {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Type definitions for the fluent migration builder API.
|
|
5
5
|
* Enables type-safe migrations without raw SQL.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type { ColumnNode, EnumValues, ForeignKeyAction } from '../../schema/types.js';
|
|
8
8
|
import type { IndexColumnInput, IndexOptions, IndexSchema } from '../../type/index.js';
|
|
9
9
|
import type { ForeignKeySchema } from '../../type/migration.js';
|
|
10
10
|
/**
|
|
@@ -68,41 +68,20 @@ export interface VectorColumnOptions extends BaseColumnOptions {
|
|
|
68
68
|
dimensions?: number;
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* A column as the builder describes one: {@link ColumnNode} without the graph links a DTO cannot carry.
|
|
72
|
+
*
|
|
73
|
+
* Derived rather than restated, so the two cannot drift. A column gained `enum` and this shape was
|
|
74
|
+
* simply missing it, which is why a hand-written `createTable` could never constrain one. A field
|
|
75
|
+
* added to the node now reaches here, and failing to render it is a compile error rather than a
|
|
76
|
+
* column that quietly loses half its declaration.
|
|
72
77
|
*/
|
|
73
|
-
export
|
|
74
|
-
/** Column name */
|
|
75
|
-
name: string;
|
|
76
|
-
/** Canonical type */
|
|
77
|
-
type: CanonicalType;
|
|
78
|
-
/** Whether the column is nullable */
|
|
79
|
-
nullable: boolean;
|
|
80
|
-
/** Default value or expression */
|
|
81
|
-
defaultValue?: unknown;
|
|
82
|
-
/** Whether this is a primary key */
|
|
83
|
-
primaryKey: boolean;
|
|
84
|
-
/** Whether this column auto-increments */
|
|
85
|
-
autoIncrement: boolean;
|
|
86
|
-
/** Whether this column has a unique constraint */
|
|
87
|
-
unique: boolean;
|
|
88
|
-
/** Column comment */
|
|
89
|
-
comment?: string;
|
|
90
|
-
}
|
|
78
|
+
export type ColumnDefinition = Omit<ColumnNode, 'table' | 'referencedBy' | 'references'>;
|
|
91
79
|
/**
|
|
92
|
-
*
|
|
80
|
+
* The foreign key a single column declares: {@link ForeignKeySchema} without the local columns, which
|
|
81
|
+
* are the column itself. Derived for the reason {@link ColumnDefinition} is - restated, the two spelled
|
|
82
|
+
* their target differently and every hand-off between them had to translate.
|
|
93
83
|
*/
|
|
94
|
-
export
|
|
95
|
-
/** Constraint name */
|
|
96
|
-
name?: string;
|
|
97
|
-
/** Referenced table */
|
|
98
|
-
table: string;
|
|
99
|
-
/** Referenced column(s) */
|
|
100
|
-
columns: string[];
|
|
101
|
-
/** Action on delete */
|
|
102
|
-
onDelete: ForeignKeyAction;
|
|
103
|
-
/** Action on update */
|
|
104
|
-
onUpdate: ForeignKeyAction;
|
|
105
|
-
}
|
|
84
|
+
export type ForeignKeyDefinition = Omit<ForeignKeySchema, 'columns'>;
|
|
106
85
|
/**
|
|
107
86
|
* Full column definition including foreign key.
|
|
108
87
|
*/
|
|
@@ -259,6 +238,10 @@ export interface IColumnBuilder {
|
|
|
259
238
|
autoIncrement(): this;
|
|
260
239
|
/** Add unique constraint */
|
|
261
240
|
unique(): this;
|
|
241
|
+
/** Constrain the column to these values, as a `CHECK (col IN (...))`. */
|
|
242
|
+
enum(values: EnumValues): this;
|
|
243
|
+
/** Make the column one the database computes, as `GENERATED ALWAYS AS (<sql>) STORED`. */
|
|
244
|
+
computed(sql: string): this;
|
|
262
245
|
/** Add comment */
|
|
263
246
|
comment(text: string): this;
|
|
264
247
|
/** Add index */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { canonicalToTypeScript } from '../../schema/canonicalType.js';
|
|
13
13
|
import { DEFAULT_FOREIGN_KEY_ACTION, } from '../../schema/types.js';
|
|
14
14
|
import { camelCase, pascalCase, singularize } from '../../util/string.util.js';
|
|
15
|
-
import { buildFieldOptionsSource } from './fieldOptionsSource.js';
|
|
15
|
+
import { buildFieldOptionsSource, fieldNeedsRaw } from './fieldOptionsSource.js';
|
|
16
16
|
import { buildIndexDecoratorSource, indexNeedsRaw, isPlainFieldIndex } from './indexDecoratorSource.js';
|
|
17
17
|
/**
|
|
18
18
|
* Generates TypeScript entity code from SchemaAST.
|
|
@@ -75,11 +75,13 @@ export class EntityCodeGenerator {
|
|
|
75
75
|
buildImports(table) {
|
|
76
76
|
const uqlImports = new Set(['Entity', 'Field']);
|
|
77
77
|
const relatedImports = [];
|
|
78
|
-
// Check for Id decorator
|
|
79
78
|
for (const col of table.columns.values()) {
|
|
80
79
|
if (col.isPrimaryKey) {
|
|
81
80
|
uqlImports.add('Id');
|
|
82
81
|
}
|
|
82
|
+
if (fieldNeedsRaw(col)) {
|
|
83
|
+
uqlImports.add('raw');
|
|
84
|
+
}
|
|
83
85
|
}
|
|
84
86
|
// Check for relation decorators
|
|
85
87
|
if (this.options.includeRelations) {
|
|
@@ -145,9 +147,6 @@ export class EntityCodeGenerator {
|
|
|
145
147
|
lines.push(' /**');
|
|
146
148
|
lines.push(` * @sync-added ${new Date().toISOString().split('T')[0]}`);
|
|
147
149
|
lines.push(` * Column: ${col.name} (${this.formatTypeDescription(col.type)})`);
|
|
148
|
-
if (col.comment) {
|
|
149
|
-
lines.push(` * ${col.comment}`);
|
|
150
|
-
}
|
|
151
150
|
lines.push(' */');
|
|
152
151
|
}
|
|
153
152
|
// Decorator
|
|
@@ -8,3 +8,5 @@ import type { ColumnNode } from '../../schema/types.js';
|
|
|
8
8
|
* file from scratch.
|
|
9
9
|
*/
|
|
10
10
|
export declare function buildFieldOptionsSource(col: ColumnNode, propertyName: string, indexName?: string): string;
|
|
11
|
+
/** Whether the field's decorator needs `raw` imported, the way {@link indexNeedsRaw} does for an index. */
|
|
12
|
+
export declare function fieldNeedsRaw(col: ColumnNode): boolean;
|
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import { canonicalToColumnType } from '../../schema/canonicalType.js';
|
|
2
|
+
import { quoted, rawTag } from './sourceLiteral.js';
|
|
3
|
+
/**
|
|
4
|
+
* What each field of a {@link ColumnNode} contributes to `@Field({ ... })`, in emit order, and `null`
|
|
5
|
+
* where nothing does.
|
|
6
|
+
*
|
|
7
|
+
* The `satisfies` is the point: a field the node gains cannot reach here without someone answering
|
|
8
|
+
* whether an entity generated from a database keeps it. Written as a hand-rolled `if` chain, this had
|
|
9
|
+
* already dropped `comment` - introspection reads one on Postgres and MySQL, and regenerating an
|
|
10
|
+
* entity threw it away.
|
|
11
|
+
*/
|
|
12
|
+
const OPTION_SOURCE = {
|
|
13
|
+
// Without this the entity maps to a column named after the property, which for anything the
|
|
14
|
+
// transformer rewrote - every `user_id` - is a column the database does not have.
|
|
15
|
+
name: (col, { propertyName }) => (propertyName === col.name ? [] : [`name: ${quoted(col.name)}`]),
|
|
16
|
+
type: (col) => {
|
|
17
|
+
const columnType = canonicalToColumnType(col.type);
|
|
18
|
+
return [
|
|
19
|
+
...(columnType ? [`columnType: ${quoted(columnType)}`] : []),
|
|
20
|
+
...(col.type.length && col.type.category === 'string' ? [`length: ${col.type.length}`] : []),
|
|
21
|
+
...(col.type.precision === undefined ? [] : [`precision: ${col.type.precision}`]),
|
|
22
|
+
...(col.type.precision !== undefined && col.type.scale !== undefined ? [`scale: ${col.type.scale}`] : []),
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
nullable: (col) => (col.nullable ? ['nullable: true'] : []),
|
|
26
|
+
isUnique: (col) => (col.isUnique ? ['unique: true'] : []),
|
|
27
|
+
enum: (col) => col.enum ? [`enum: [${col.enum.map((it) => (typeof it === 'number' ? it : quoted(it))).join(', ')}]`] : [],
|
|
28
|
+
defaultValue: (col) => col.defaultValue === undefined ? [] : [`defaultValue: ${defaultValueSource(col.defaultValue)}`],
|
|
29
|
+
generatedAs: (col) => (col.generatedAs ? [`computed: ${rawTag(col.generatedAs)}`, 'stored: true'] : []),
|
|
30
|
+
comment: (col) => (col.comment ? [`comment: ${quoted(col.comment)}`] : []),
|
|
31
|
+
// A key is `@Id`, and a numeric one generates by that alone. Neither is an option to write out.
|
|
32
|
+
isPrimaryKey: null,
|
|
33
|
+
isAutoIncrement: null,
|
|
34
|
+
// Graph links. A foreign key becomes a relation decorator, emitted beside the field rather than in it.
|
|
35
|
+
table: null,
|
|
36
|
+
references: null,
|
|
37
|
+
referencedBy: null,
|
|
38
|
+
};
|
|
2
39
|
/**
|
|
3
40
|
* A column's `@Field({ ... })` options as source, or `''` when it needs none.
|
|
4
41
|
*
|
|
@@ -8,47 +45,26 @@ import { canonicalToColumnType } from '../../schema/canonicalType.js';
|
|
|
8
45
|
* file from scratch.
|
|
9
46
|
*/
|
|
10
47
|
export function buildFieldOptionsSource(col, propertyName, indexName) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const columnType = canonicalToColumnType(col.type);
|
|
18
|
-
if (columnType) {
|
|
19
|
-
options.push(`columnType: '${columnType}'`);
|
|
20
|
-
}
|
|
21
|
-
if (col.type.length && col.type.category === 'string') {
|
|
22
|
-
options.push(`length: ${col.type.length}`);
|
|
23
|
-
}
|
|
24
|
-
if (col.type.precision !== undefined) {
|
|
25
|
-
options.push(`precision: ${col.type.precision}`);
|
|
26
|
-
if (col.type.scale !== undefined) {
|
|
27
|
-
options.push(`scale: ${col.type.scale}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (col.nullable) {
|
|
31
|
-
options.push('nullable: true');
|
|
32
|
-
}
|
|
33
|
-
if (col.isUnique) {
|
|
34
|
-
options.push('unique: true');
|
|
35
|
-
}
|
|
36
|
-
if (col.defaultValue !== undefined) {
|
|
37
|
-
options.push(`defaultValue: ${formatDefaultValueSource(col.defaultValue)}`);
|
|
38
|
-
}
|
|
39
|
-
if (indexName) {
|
|
40
|
-
options.push(`index: '${indexName}'`);
|
|
41
|
-
}
|
|
48
|
+
const context = { propertyName, indexName };
|
|
49
|
+
const options = [
|
|
50
|
+
...Object.values(OPTION_SOURCE).flatMap((source) => source?.(col, context) ?? []),
|
|
51
|
+
// Not a column field: the index is a table-level object the field only borrows.
|
|
52
|
+
...(indexName ? [`index: ${quoted(indexName)}`] : []),
|
|
53
|
+
];
|
|
42
54
|
return options.length > 0 ? `{ ${options.join(', ')} }` : '';
|
|
43
55
|
}
|
|
56
|
+
/** Whether the field's decorator needs `raw` imported, the way {@link indexNeedsRaw} does for an index. */
|
|
57
|
+
export function fieldNeedsRaw(col) {
|
|
58
|
+
return col.generatedAs !== undefined;
|
|
59
|
+
}
|
|
44
60
|
/**
|
|
45
61
|
* A default value as source. Strings stay single-quoted, expressions included: `defaultValue: 'now()'`
|
|
46
62
|
* is what reaches the DDL. The generator used to branch on `CURRENT_TIMESTAMP`/`NEXTVAL`/`(` first, but
|
|
47
63
|
* both branches emitted a quoted string and only the fallthrough escaped embedded quotes.
|
|
48
64
|
*/
|
|
49
|
-
function
|
|
65
|
+
function defaultValueSource(value) {
|
|
50
66
|
if (typeof value === 'string') {
|
|
51
|
-
return
|
|
67
|
+
return quoted(value);
|
|
52
68
|
}
|
|
53
69
|
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
54
70
|
return value.toString();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rawTag } from './sourceLiteral.js';
|
|
1
2
|
/**
|
|
2
3
|
* A vector index carries its metric in the operator class pgvector names after it
|
|
3
4
|
* (`vector_cosine_ops`), which is the only place introspection can recover it from. `@Index` requires
|
|
@@ -94,12 +95,3 @@ function indexEntrySource(entry, propertyName) {
|
|
|
94
95
|
}
|
|
95
96
|
return `{ column: '${propertyName(entry.column)}', ${modifiers.join(', ')} }`;
|
|
96
97
|
}
|
|
97
|
-
/**
|
|
98
|
-
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
99
|
-
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
|
100
|
-
* Newlines need none, which keeps a multi-line expression readable in the generated entity.
|
|
101
|
-
*/
|
|
102
|
-
function rawTag(sql) {
|
|
103
|
-
const escaped = sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
104
|
-
return `raw\`${escaped}\``;
|
|
105
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A string as single-quoted source. Introspected text is arbitrary - a comment or a default
|
|
3
|
+
* expression can hold a quote or a backslash - and only escaping both keeps the generated file
|
|
4
|
+
* parsing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function quoted(text: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
9
|
+
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
|
10
|
+
* Newlines need none, which keeps a multi-line expression readable in the generated entity.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rawTag(sql: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A string as single-quoted source. Introspected text is arbitrary - a comment or a default
|
|
3
|
+
* expression can hold a quote or a backslash - and only escaping both keeps the generated file
|
|
4
|
+
* parsing.
|
|
5
|
+
*/
|
|
6
|
+
export function quoted(text) {
|
|
7
|
+
return `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
11
|
+
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
|
12
|
+
* Newlines need none, which keeps a multi-line expression readable in the generated entity.
|
|
13
|
+
*/
|
|
14
|
+
export function rawTag(sql) {
|
|
15
|
+
const escaped = sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
16
|
+
return `raw\`${escaped}\``;
|
|
17
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnNode, TableNode } from '../../schema/types.js';
|
|
2
|
+
import type { ForeignKeySchema, IndexSchema } from '../../type/migration.js';
|
|
2
3
|
import type { FullColumnDefinition, TableDefinition } from '../builder/types.js';
|
|
3
4
|
/**
|
|
4
5
|
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
@@ -6,4 +7,24 @@ import type { FullColumnDefinition, TableDefinition } from '../builder/types.js'
|
|
|
6
7
|
* not generator methods: nothing here consults the dialect.
|
|
7
8
|
*/
|
|
8
9
|
export declare function tableDefinitionToNode(def: TableDefinition): TableNode;
|
|
10
|
+
/**
|
|
11
|
+
* A builder's column as the AST node the generators render from.
|
|
12
|
+
*
|
|
13
|
+
* The shared half is spread, not copied field by field: `ColumnDefinition` *is* a `ColumnNode` minus
|
|
14
|
+
* the graph links, so spreading it and adding those back is a node by construction. Listed one by one,
|
|
15
|
+
* the copy silently dropped whatever the node gained next - `enum` first, and the type had no way to
|
|
16
|
+
* say so. The two builder-only keys are destructured off: `index` and `foreignKey` are lifted onto the
|
|
17
|
+
* table by `columnIndex`/`columnForeignKey`, which is the path that renders them.
|
|
18
|
+
*
|
|
19
|
+
* No `references` node either: `SchemaAST.addRelationship` sets that one.
|
|
20
|
+
*/
|
|
9
21
|
export declare function fullColumnDefinitionToNode(col: FullColumnDefinition, tableName: string): ColumnNode;
|
|
22
|
+
/**
|
|
23
|
+
* The index a column-level `index` declares, or nothing.
|
|
24
|
+
*
|
|
25
|
+
* Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
|
|
26
|
+
* `addColumn` had no lift at all and silently emitted a column with no index.
|
|
27
|
+
*/
|
|
28
|
+
export declare function columnIndex(tableName: string, col: FullColumnDefinition): IndexSchema | undefined;
|
|
29
|
+
/** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
|
|
30
|
+
export declare function columnForeignKey(col: FullColumnDefinition): ForeignKeySchema | undefined;
|