uql-orm 0.22.0 → 0.23.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.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +14 -1
- package/dist/dialect/abstractSqlDialect.js +21 -14
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +11 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +25 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +4 -7
- package/dist/dialect/pgLikeSqlDialect.js +7 -12
- package/dist/entity/decorator/bag.d.ts +35 -0
- package/dist/entity/decorator/bag.js +54 -0
- package/dist/entity/decorator/entity.d.ts +28 -2
- package/dist/entity/decorator/entity.js +40 -2
- package/dist/entity/decorator/members.d.ts +51 -0
- package/dist/entity/decorator/members.js +51 -0
- package/dist/entity/index.d.ts +3 -1
- package/dist/entity/index.js +3 -1
- package/dist/entity/metadata/definition.d.ts +23 -9
- package/dist/entity/metadata/definition.js +55 -100
- package/dist/http/contract.js +1 -2
- package/dist/http/handler.js +5 -26
- package/dist/http/query.js +1 -1
- package/dist/migrate/acquireQuerierForMigrations.d.ts +13 -1
- package/dist/migrate/acquireQuerierForMigrations.js +28 -0
- package/dist/migrate/cli-config.js +24 -8
- package/dist/migrate/cli.js +5 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +3 -4
- package/dist/migrate/codegen/index.d.ts +0 -1
- package/dist/migrate/codegen/index.js +0 -2
- package/dist/migrate/drift/driftDetector.d.ts +10 -1
- package/dist/migrate/drift/driftDetector.js +22 -5
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +17 -27
- package/dist/migrate/introspection/mongoIntrospector.js +7 -13
- package/dist/migrate/migrator.js +44 -81
- package/dist/migrate/storage/databaseStorage.d.ts +2 -0
- package/dist/migrate/storage/databaseStorage.js +8 -23
- package/dist/mongo/mongodbQuerier.js +171 -229
- package/dist/nestjs/uqlContextInterceptor.js +58 -22
- package/dist/nestjs/uqlModule.d.ts +2 -11
- package/dist/nestjs/uqlModule.js +93 -44
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +28 -0
- package/dist/querier/abstractQuerier.js +47 -22
- package/dist/querier/abstractQuerierPool.d.ts +3 -0
- package/dist/querier/abstractQuerierPool.js +5 -3
- package/dist/querier/abstractSqlQuerier.d.ts +1 -3
- package/dist/querier/abstractSqlQuerier.js +48 -96
- package/dist/querier/index.d.ts +3 -1
- package/dist/querier/index.js +3 -1
- package/dist/querier/querierContext.browser.d.ts +12 -0
- package/dist/querier/querierContext.browser.js +18 -0
- package/dist/querier/querierContext.d.ts +22 -0
- package/dist/querier/querierContext.js +42 -0
- package/dist/querier/queryError.d.ts +22 -0
- package/dist/querier/queryError.js +20 -0
- package/dist/querier/transactional.d.ts +26 -0
- package/dist/querier/transactional.js +43 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.js +2 -2
- package/dist/schema/types.d.ts +0 -21
- package/dist/sqlite/abstractSqliteQuerier.d.ts +9 -2
- package/dist/sqlite/hranaQuerier.js +20 -42
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/localSqliteQuerierPool.d.ts +31 -0
- package/dist/sqlite/localSqliteQuerierPool.js +34 -0
- package/dist/sqlite/nodeSqliteAdapter.d.ts +35 -0
- package/dist/sqlite/nodeSqliteAdapter.js +28 -0
- package/dist/sqlite/nodeSqliteQuerierPool.d.ts +28 -0
- package/dist/sqlite/nodeSqliteQuerierPool.js +29 -0
- package/dist/sqlite/sqliteDialect.d.ts +1 -2
- package/dist/sqlite/sqliteDialect.js +0 -13
- package/dist/sqlite/sqliteQuerierPool.d.ts +10 -23
- package/dist/sqlite/sqliteQuerierPool.js +9 -28
- package/dist/type/entity.d.ts +95 -28
- package/dist/type/querier.d.ts +6 -0
- package/dist/util/hook.util.js +1 -1
- package/package.json +8 -16
- package/dist/entity/decorator/field.d.ts +0 -2
- package/dist/entity/decorator/field.js +0 -7
- package/dist/entity/decorator/filter.d.ts +0 -7
- package/dist/entity/decorator/filter.js +0 -11
- package/dist/entity/decorator/hook.d.ts +0 -7
- package/dist/entity/decorator/hook.js +0 -14
- package/dist/entity/decorator/id.d.ts +0 -2
- package/dist/entity/decorator/id.js +0 -7
- package/dist/entity/decorator/index-decorator.d.ts +0 -22
- package/dist/entity/decorator/index-decorator.js +0 -26
- package/dist/entity/decorator/index.d.ts +0 -8
- package/dist/entity/decorator/index.js +0 -8
- package/dist/entity/decorator/relation.d.ts +0 -8
- package/dist/entity/decorator/relation.js +0 -19
- package/dist/migrate/codegen/smartRelationDetector.d.ts +0 -47
- package/dist/migrate/codegen/smartRelationDetector.js +0 -134
- package/dist/querier/decorator/index.d.ts +0 -4
- package/dist/querier/decorator/index.js +0 -4
- package/dist/querier/decorator/injectQuerier.d.ts +0 -3
- package/dist/querier/decorator/injectQuerier.js +0 -33
- package/dist/querier/decorator/log.d.ts +0 -32
- package/dist/querier/decorator/log.js +0 -52
- package/dist/querier/decorator/serialized.d.ts +0 -6
- package/dist/querier/decorator/serialized.js +0 -13
- package/dist/querier/decorator/transactional.d.ts +0 -6
- package/dist/querier/decorator/transactional.js +0 -48
|
@@ -1,52 +1,45 @@
|
|
|
1
1
|
import { getKeys, hasKeys, lowerFirst, normalizeIndexColumn, upperFirst } from '../../util/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ownRegistrations } from '../decorator/bag.js';
|
|
3
3
|
// Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
|
|
4
|
-
// evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading).
|
|
4
|
+
// evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
|
|
5
|
+
// because v1 changed the `FieldOptions` shape: a tree holding both majors gets two maps rather than one
|
|
6
|
+
// map with entries the other major cannot read.
|
|
5
7
|
const holder = globalThis;
|
|
6
|
-
const metaKey = Symbol.for('uql-orm/entity/metadata');
|
|
8
|
+
const metaKey = Symbol.for('uql-orm/entity/metadata/v1');
|
|
7
9
|
const metas = holder[metaKey] ?? new Map();
|
|
8
10
|
holder[metaKey] = metas;
|
|
9
|
-
const registrationLogger = new LoggerWrapper(true);
|
|
10
|
-
/**
|
|
11
|
-
* Append a composite index entry with normalized `unique` (default false) and columns. Normalizing
|
|
12
|
-
* here is what lets the dialects render one shape instead of re-parsing the authored sugar.
|
|
13
|
-
*/
|
|
14
|
-
export function appendEntityIndex(meta, index) {
|
|
15
|
-
if (!meta.indexes)
|
|
16
|
-
meta.indexes = [];
|
|
17
|
-
meta.indexes.push({ ...index, unique: index.unique ?? false, columns: index.columns.map(normalizeIndexColumn) });
|
|
18
|
-
}
|
|
19
11
|
export function defineField(entity, key, opts = {}) {
|
|
20
12
|
const meta = ensureMeta(entity);
|
|
21
|
-
if (opts.type) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
else {
|
|
25
|
-
opts = { ...opts, type: inferType(entity, key), typeInferred: true };
|
|
13
|
+
if (!opts.type && !opts.references && !opts.virtual) {
|
|
14
|
+
throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
|
|
15
|
+
"at another entity with 'references', which resolves the column type from its primary key.");
|
|
26
16
|
}
|
|
27
17
|
const fieldKey = key;
|
|
28
|
-
|
|
18
|
+
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
19
|
+
// column from the referenced primary key (picking up its `columnType`, length and chained keys)
|
|
20
|
+
// instead of treating whatever ends up in `type` as deliberate.
|
|
21
|
+
const resolved = opts.type ? opts : { ...opts, typeFromReference: true };
|
|
22
|
+
meta.fields[fieldKey] = { ...meta.fields[fieldKey], ...{ name: key, ...resolved } };
|
|
29
23
|
return meta;
|
|
30
24
|
}
|
|
31
25
|
export function defineId(entity, key, opts) {
|
|
32
26
|
const meta = ensureMeta(entity);
|
|
33
27
|
const id = getIdKey(meta);
|
|
34
28
|
if (id) {
|
|
35
|
-
|
|
29
|
+
// A subclass narrowing the inherited primary key: drop the old one so exactly one stays marked.
|
|
36
30
|
delete meta.fields[id];
|
|
37
31
|
}
|
|
38
32
|
return defineField(entity, key, { ...opts, isId: true });
|
|
39
33
|
}
|
|
34
|
+
// `RelationOptions` is parameterized by the *target* entity, which is independent of the owner `E`, so it
|
|
35
|
+
// is left at its default here rather than tied to the class being registered.
|
|
40
36
|
export function defineRelation(entity, key, opts) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const inferredType = inferEntityType(entity, key);
|
|
45
|
-
return { ...opts, entity: () => inferredType };
|
|
46
|
-
})();
|
|
37
|
+
if (!opts.entity) {
|
|
38
|
+
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
39
|
+
}
|
|
47
40
|
const meta = ensureMeta(entity);
|
|
48
41
|
const relKey = key;
|
|
49
|
-
meta.relations[relKey] = { ...meta.relations[relKey], ...
|
|
42
|
+
meta.relations[relKey] = { ...meta.relations[relKey], ...opts };
|
|
50
43
|
return meta;
|
|
51
44
|
}
|
|
52
45
|
export function defineHook(entity, methodName, event) {
|
|
@@ -58,9 +51,15 @@ export function defineHook(entity, methodName, event) {
|
|
|
58
51
|
meta.hooks[event].push({ methodName });
|
|
59
52
|
return meta;
|
|
60
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Declares a composite index. `unique` and the authored column sugar are normalized here, which is what
|
|
56
|
+
* lets the dialects render one shape instead of re-parsing it.
|
|
57
|
+
*/
|
|
61
58
|
export function defineIndex(entity, index) {
|
|
62
59
|
const meta = ensureMeta(entity);
|
|
63
|
-
|
|
60
|
+
if (!meta.indexes)
|
|
61
|
+
meta.indexes = [];
|
|
62
|
+
meta.indexes.push({ ...index, unique: index.unique ?? false, columns: index.columns.map(normalizeIndexColumn) });
|
|
64
63
|
return meta;
|
|
65
64
|
}
|
|
66
65
|
export function defineFilter(entity, name, opts) {
|
|
@@ -76,9 +75,12 @@ export function defineFilter(entity, name, opts) {
|
|
|
76
75
|
meta.filters[name] = opts;
|
|
77
76
|
return meta;
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
|
|
80
|
+
* API converge on one registration path before anything is finalized.
|
|
81
|
+
*/
|
|
82
|
+
export function applyMembers(entity, specs) {
|
|
83
|
+
for (const [key, spec] of Object.entries(specs?.fields ?? {})) {
|
|
82
84
|
if (!spec)
|
|
83
85
|
continue;
|
|
84
86
|
if (spec.isId) {
|
|
@@ -88,64 +90,43 @@ function applyBulkFields(entity, fields) {
|
|
|
88
90
|
defineField(entity, key, spec);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
|
-
}
|
|
92
|
-
function applyBulkRelations(entity, relations) {
|
|
93
|
-
for (const key of Object.keys(relations)) {
|
|
94
|
-
const spec = relations[key];
|
|
93
|
+
for (const [key, spec] of Object.entries(specs?.relations ?? {})) {
|
|
95
94
|
if (spec)
|
|
96
95
|
defineRelation(entity, key, spec);
|
|
97
96
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const meta = ensureMeta(entity);
|
|
101
|
-
for (const idx of indexes) {
|
|
102
|
-
appendEntityIndex(meta, idx);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function applyBulkHooks(entity, hooks) {
|
|
106
|
-
for (const event of Object.keys(hooks)) {
|
|
107
|
-
const methodNames = hooks[event];
|
|
108
|
-
if (!methodNames?.length)
|
|
109
|
-
continue;
|
|
110
|
-
for (const methodName of methodNames) {
|
|
97
|
+
for (const [event, methodNames] of Object.entries(specs?.hooks ?? {})) {
|
|
98
|
+
for (const methodName of methodNames ?? []) {
|
|
111
99
|
defineHook(entity, methodName, event);
|
|
112
100
|
}
|
|
113
101
|
}
|
|
114
102
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
export function defineEntity(entity, opts = {}) {
|
|
104
|
+
const meta = ensureMeta(entity);
|
|
105
|
+
// Covers `defineEntity(Decorated)` called on a class whose members carry decorators. `@Entity()`
|
|
106
|
+
// drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
|
|
107
|
+
// after class decorators return; draining empties the bag, so whichever runs second is a no-op.
|
|
108
|
+
applyMembers(entity, ownRegistrations(entity));
|
|
109
|
+
applyMembers(entity, opts);
|
|
110
|
+
for (const index of opts.indexes ?? []) {
|
|
111
|
+
defineIndex(entity, index);
|
|
112
|
+
}
|
|
113
|
+
for (const [name, spec] of Object.entries(opts.filters ?? {})) {
|
|
118
114
|
if (spec)
|
|
119
115
|
defineFilter(entity, name, spec);
|
|
120
116
|
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Applies `fields`, `relations`, `indexes`, and `hooks` from {@link EntityOptions} before
|
|
124
|
-
* entity finalization. Used by `defineEntity` for decorator-free registration.
|
|
125
|
-
*/
|
|
126
|
-
function applyBulkEntityOptions(entity, opts) {
|
|
127
|
-
if (opts.fields)
|
|
128
|
-
applyBulkFields(entity, opts.fields);
|
|
129
|
-
if (opts.relations)
|
|
130
|
-
applyBulkRelations(entity, opts.relations);
|
|
131
|
-
if (opts.indexes?.length)
|
|
132
|
-
applyBulkIndexes(entity, opts.indexes);
|
|
133
|
-
if (opts.hooks)
|
|
134
|
-
applyBulkHooks(entity, opts.hooks);
|
|
135
|
-
if (opts.filters)
|
|
136
|
-
applyBulkFilters(entity, opts.filters);
|
|
137
|
-
}
|
|
138
|
-
export function defineEntity(entity, opts = {}) {
|
|
139
|
-
const meta = ensureMeta(entity);
|
|
140
|
-
applyBulkEntityOptions(entity, opts);
|
|
141
117
|
if (!hasKeys(meta.fields)) {
|
|
142
118
|
throw TypeError(`'${entity.name}' must have fields`);
|
|
143
119
|
}
|
|
144
120
|
meta.name = opts.name ?? entity.name;
|
|
145
121
|
let proto = Object.getPrototypeOf(entity.prototype);
|
|
146
122
|
while (proto.constructor !== Object) {
|
|
147
|
-
const
|
|
148
|
-
|
|
123
|
+
const parent = proto.constructor;
|
|
124
|
+
// An `abstract class BaseEntity` carrying `@Field`s but no `@Entity()` has nobody to drain its
|
|
125
|
+
// registrations, so do it here. Walking the *class* prototype chain rather than reading through the
|
|
126
|
+
// metadata object's is what makes this work on every transformer: tsc and esbuild chain metadata
|
|
127
|
+
// across `extends`, SWC does not.
|
|
128
|
+
applyMembers(parent, ownRegistrations(parent));
|
|
129
|
+
extendMeta(meta, ensureMeta(parent));
|
|
149
130
|
proto = Object.getPrototypeOf(proto);
|
|
150
131
|
}
|
|
151
132
|
// Derive soft-delete from the (inheritance-merged) fields, so own and inherited markers are handled
|
|
@@ -241,7 +222,7 @@ function fillRelations(meta) {
|
|
|
241
222
|
name: fkKey,
|
|
242
223
|
type: relatedIdField?.type ?? Number,
|
|
243
224
|
references: relOpts.entity,
|
|
244
|
-
|
|
225
|
+
typeFromReference: true,
|
|
245
226
|
};
|
|
246
227
|
}
|
|
247
228
|
}
|
|
@@ -342,29 +323,3 @@ function extendMeta(target, source) {
|
|
|
342
323
|
}
|
|
343
324
|
}
|
|
344
325
|
}
|
|
345
|
-
/** Reads the `design:type` emitted by `emitDecoratorMetadata`, if the optional polyfill is loaded. */
|
|
346
|
-
function inferType(entity, key) {
|
|
347
|
-
const { getMetadata } = Reflect;
|
|
348
|
-
if (!getMetadata) {
|
|
349
|
-
throw new TypeError(`'${entity.name}.${key}' has no explicit type and 'design:type' metadata is unavailable. Either declare the type - '@Field({ type: String })' or '@Relation({ entity: () => Other })' - or install 'reflect-metadata' and add 'import "reflect-metadata"' once at your app entry point.`);
|
|
350
|
-
}
|
|
351
|
-
return getMetadata('design:type', entity.prototype, key);
|
|
352
|
-
}
|
|
353
|
-
// biome-ignore lint/suspicious/noExplicitAny: reflected type is unknown at compile time
|
|
354
|
-
function inferEntityType(entity, key) {
|
|
355
|
-
const inferredType = inferType(entity, key);
|
|
356
|
-
const isValidType = isValidEntityType(inferredType);
|
|
357
|
-
if (!isValidType) {
|
|
358
|
-
throw TypeError(`'${entity.name}.${key}' type was auto-inferred with invalid type '${inferredType?.name}'`);
|
|
359
|
-
}
|
|
360
|
-
return inferredType;
|
|
361
|
-
}
|
|
362
|
-
export function isValidEntityType(type) {
|
|
363
|
-
return (typeof type === 'function' &&
|
|
364
|
-
type !== Boolean &&
|
|
365
|
-
type !== String &&
|
|
366
|
-
type !== Number &&
|
|
367
|
-
type !== BigInt &&
|
|
368
|
-
type !== Date &&
|
|
369
|
-
type !== Symbol);
|
|
370
|
-
}
|
package/dist/http/contract.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// metadata and reflect-metadata out of the browser bundle
|
|
1
|
+
// the specific util modules, not the barrel, so the browser bundle does not pull in entity metadata
|
|
3
2
|
import { getKeys } from '../util/object.util.js';
|
|
4
3
|
import { kebabCase } from '../util/string.util.js';
|
|
5
4
|
/**
|
package/dist/http/handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
2
|
import { getEntities, getMeta } from '../entity/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getQuerierPool } from '../options.js';
|
|
4
4
|
import { entityPath, matchRoute, } from './contract.js';
|
|
5
5
|
import { parseQueryParams } from './query.js';
|
|
6
6
|
export function createRequestHandler(opts = {}) {
|
|
@@ -135,31 +135,10 @@ export function createRequestHandler(opts = {}) {
|
|
|
135
135
|
function ok(body) {
|
|
136
136
|
return { status: 200, body };
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
await querier.release();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async function withTransaction(fn) {
|
|
148
|
-
const querier = await getQuerier();
|
|
149
|
-
try {
|
|
150
|
-
await querier.beginTransaction();
|
|
151
|
-
const resp = await fn(querier);
|
|
152
|
-
await querier.commitTransaction();
|
|
153
|
-
return resp;
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
await querier.rollbackTransaction().catch(() => { });
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
await querier.release();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
138
|
+
/** Read paths: the pool acquires and releases; nothing here owns a connection. */
|
|
139
|
+
const withQuerier = (fn) => getQuerierPool().withQuerier(fn);
|
|
140
|
+
/** Write paths: same, plus commit on success and rollback on failure. */
|
|
141
|
+
const withTransaction = (fn) => getQuerierPool().transaction(fn);
|
|
163
142
|
function buildIdQuery(meta, id, query) {
|
|
164
143
|
const idKey = meta.id;
|
|
165
144
|
const where = query.$where;
|
package/dist/http/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// specific util module
|
|
1
|
+
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
2
2
|
import { getKeys } from '../util/object.util.js';
|
|
3
3
|
const JSON_QUERY_KEYS = [
|
|
4
4
|
'$select',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Querier, type QuerierPool, type SqlQuerier } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Querier used for schema migrations and the migration journal (`DatabaseMigrationStorage`).
|
|
4
4
|
*
|
|
@@ -6,3 +6,15 @@ import type { Querier, QuerierPool } from '../type/index.js';
|
|
|
6
6
|
* app traffic (e.g. LibSQL embedded replica: local `file:` + remote `syncUrl`).
|
|
7
7
|
*/
|
|
8
8
|
export declare function acquireQuerierForMigrations(pool: QuerierPool): Promise<Querier>;
|
|
9
|
+
/**
|
|
10
|
+
* Runs `task` on a migration querier and releases it, whatever happens.
|
|
11
|
+
*
|
|
12
|
+
* `pool.withQuerier` cannot serve here because migrations may run on a different connection than app
|
|
13
|
+
* traffic, but the ownership rule is the same one: whoever acquires, releases.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withQuerierForMigrations<T>(pool: QuerierPool, task: (querier: Querier) => Promise<T>): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Same, for the paths that only work against SQL. `requiredBy` names the caller in the error, which is
|
|
18
|
+
* the only thing the five copies of this acquire-assert-release dance used to differ by.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withSqlQuerierForMigrations<T>(pool: QuerierPool, requiredBy: string, task: (querier: SqlQuerier) => Promise<T>): Promise<T>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSqlQuerier } from '../type/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Querier used for schema migrations and the migration journal (`DatabaseMigrationStorage`).
|
|
3
4
|
*
|
|
@@ -7,3 +8,30 @@
|
|
|
7
8
|
export async function acquireQuerierForMigrations(pool) {
|
|
8
9
|
return (await pool.getMigrationQuerier?.()) ?? (await pool.getQuerier());
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Runs `task` on a migration querier and releases it, whatever happens.
|
|
13
|
+
*
|
|
14
|
+
* `pool.withQuerier` cannot serve here because migrations may run on a different connection than app
|
|
15
|
+
* traffic, but the ownership rule is the same one: whoever acquires, releases.
|
|
16
|
+
*/
|
|
17
|
+
export async function withQuerierForMigrations(pool, task) {
|
|
18
|
+
const querier = await acquireQuerierForMigrations(pool);
|
|
19
|
+
try {
|
|
20
|
+
return await task(querier);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await querier.release();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Same, for the paths that only work against SQL. `requiredBy` names the caller in the error, which is
|
|
28
|
+
* the only thing the five copies of this acquire-assert-release dance used to differ by.
|
|
29
|
+
*/
|
|
30
|
+
export function withSqlQuerierForMigrations(pool, requiredBy, task) {
|
|
31
|
+
return withQuerierForMigrations(pool, (querier) => {
|
|
32
|
+
if (!isSqlQuerier(querier)) {
|
|
33
|
+
throw new TypeError(`${requiredBy} requires a SQL-based querier`);
|
|
34
|
+
}
|
|
35
|
+
return task(querier);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Loads the config with a plain `import()`, leaving TypeScript to whatever runs the CLI.
|
|
6
|
+
*
|
|
7
|
+
* @remarks uql deliberately bundles no transpiler. The config imports the entity classes, so whoever
|
|
8
|
+
* loads it decides which decorator spec their decorators are invoked with, and only the runtime knows
|
|
9
|
+
* the project's `tsconfig.json`. Bun and `node --import tsx` both get it right; a bundled loader would
|
|
10
|
+
* be guessing, and `jiti` guessed wrong (it hardcodes the legacy transform, so standard decorators were
|
|
11
|
+
* called as `(prototype, key)` and every field was silently dropped).
|
|
12
|
+
*
|
|
13
|
+
* Node's own type stripping covers a config that is only types plus a plain object, which is why the
|
|
14
|
+
* error below distinguishes the two cases: decorators are not erasable syntax, so a config that reaches
|
|
15
|
+
* decorated entity classes needs a runtime that actually transforms them.
|
|
16
|
+
*/
|
|
4
17
|
async function importConfig(path) {
|
|
5
|
-
const
|
|
6
|
-
throw new
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
const mod = (await import(pathToFileURL(path).href).catch((cause) => {
|
|
19
|
+
throw new TypeError(`Could not import ${path}: ${cause?.message}\n` +
|
|
20
|
+
'If it reaches entity classes, their decorators need a runtime that transforms TypeScript, not ' +
|
|
21
|
+
'just one that strips its types. Run the CLI with `bun`, or with `node --import tsx` ' +
|
|
22
|
+
'(`npm i -D tsx`). A JavaScript config, or passing the config inline, needs neither.', { cause });
|
|
23
|
+
}));
|
|
24
|
+
return mod.default ?? mod;
|
|
9
25
|
}
|
|
10
26
|
export async function loadConfig(customPath) {
|
|
11
27
|
if (customPath) {
|
|
@@ -14,14 +30,14 @@ export async function loadConfig(customPath) {
|
|
|
14
30
|
.then(() => true)
|
|
15
31
|
.catch(() => false);
|
|
16
32
|
if (!exists) {
|
|
17
|
-
throw new
|
|
33
|
+
throw new TypeError(`Could not find uql configuration file at ${customPath}`);
|
|
18
34
|
}
|
|
19
35
|
try {
|
|
20
36
|
const config = await importConfig(fullPath);
|
|
21
37
|
return config;
|
|
22
38
|
}
|
|
23
39
|
catch (error) {
|
|
24
|
-
throw new
|
|
40
|
+
throw new TypeError(`Could not load configuration file at ${customPath}: ${error.message}`);
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
const configPaths = ['uql.config.ts', 'uql.config.js', 'uql.config.mjs', '.uqlrc.ts', '.uqlrc.js'];
|
|
@@ -35,5 +51,5 @@ export async function loadConfig(customPath) {
|
|
|
35
51
|
return config;
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
|
-
throw new
|
|
54
|
+
throw new TypeError('Could not find uql configuration file. Create a uql.config.ts or uql.config.js file in your project root.');
|
|
39
55
|
}
|
package/dist/migrate/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { detectDrift } from './drift/driftDetector.js';
|
|
|
9
9
|
import { Migrator } from './migrator.js';
|
|
10
10
|
import { createSchemaGenerator } from './schemaGenerator.js';
|
|
11
11
|
import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
|
|
12
|
+
import { DEFAULT_MIGRATIONS_TABLE } from './storage/databaseStorage.js';
|
|
12
13
|
/** Sync helper for SQL dialects only; returns `undefined` for MongoDB - use {@link createSchemaGeneratorAsync}. */
|
|
13
14
|
export function getSchemaGenerator(dialect, defaultForeignKeyAction) {
|
|
14
15
|
return createSchemaGenerator(dialect, defaultForeignKeyAction);
|
|
@@ -265,7 +266,10 @@ export async function runDriftCheck(migrator, config) {
|
|
|
265
266
|
const actualAST = await migrator.schemaIntrospector.introspect();
|
|
266
267
|
// Detect drift. The dialect renders canonical types as SQL - without it every type formats as
|
|
267
268
|
// `unknown` and type drift compares equal, silently reporting a mismatched column as in sync.
|
|
268
|
-
const report = detectDrift(expectedAST, actualAST, {
|
|
269
|
+
const report = detectDrift(expectedAST, actualAST, {
|
|
270
|
+
dialect: config.pool?.dialect,
|
|
271
|
+
excludeTables: [config.tableName ?? DEFAULT_MIGRATIONS_TABLE],
|
|
272
|
+
});
|
|
269
273
|
printDriftReport(report);
|
|
270
274
|
}
|
|
271
275
|
}
|
|
@@ -83,7 +83,6 @@ export class EntityCodeGenerator {
|
|
|
83
83
|
if (this.options.includeRelations) {
|
|
84
84
|
for (const rel of [...table.incomingRelations, ...table.outgoingRelations]) {
|
|
85
85
|
uqlImports.add(this.getRelationDecoratorName(rel.type));
|
|
86
|
-
uqlImports.add('Relation');
|
|
87
86
|
const relatedTable = rel.from.table === table ? rel.to.table : rel.from.table;
|
|
88
87
|
const relatedClassName = this.options.classNameTransformer(relatedTable.name);
|
|
89
88
|
if (!relatedImports.includes(relatedClassName)) {
|
|
@@ -238,7 +237,7 @@ export class EntityCodeGenerator {
|
|
|
238
237
|
// Decorator
|
|
239
238
|
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName} })`);
|
|
240
239
|
// Property
|
|
241
|
-
lines.push(` ${propertyName}?:
|
|
240
|
+
lines.push(` ${propertyName}?: ${relatedClassName};`);
|
|
242
241
|
return lines.join('\n');
|
|
243
242
|
}
|
|
244
243
|
/**
|
|
@@ -262,10 +261,10 @@ export class EntityCodeGenerator {
|
|
|
262
261
|
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}, references: '${inverseProp}' })`);
|
|
263
262
|
// Property
|
|
264
263
|
if (inverseType === 'OneToMany' || inverseType === 'ManyToMany') {
|
|
265
|
-
lines.push(` ${propertyName}?:
|
|
264
|
+
lines.push(` ${propertyName}?: ${relatedClassName}[];`);
|
|
266
265
|
}
|
|
267
266
|
else {
|
|
268
|
-
lines.push(` ${propertyName}?:
|
|
267
|
+
lines.push(` ${propertyName}?: ${relatedClassName};`);
|
|
269
268
|
}
|
|
270
269
|
return lines.join('\n');
|
|
271
270
|
}
|
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { createEntityCodeGenerator, EntityCodeGenerator, type EntityCodeGeneratorOptions, type GeneratedEntity, } from './entityCodeGenerator.js';
|
|
7
7
|
export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, type SqlMigrationModuleOptions, } from './migrationFile.js';
|
|
8
|
-
export { createRelationDetector, type RelationDetectorOptions, SmartRelationDetector, } from './smartRelationDetector.js';
|
|
@@ -6,5 +6,3 @@
|
|
|
6
6
|
// Entity code generator
|
|
7
7
|
export { createEntityCodeGenerator, EntityCodeGenerator, } from './entityCodeGenerator.js';
|
|
8
8
|
export { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCall, emitSqlRunCalls, } from './migrationFile.js';
|
|
9
|
-
// Smart relation detector
|
|
10
|
-
export { createRelationDetector, SmartRelationDetector, } from './smartRelationDetector.js';
|
|
@@ -19,8 +19,17 @@ export interface DriftDetectorOptions {
|
|
|
19
19
|
checkIndexes?: boolean;
|
|
20
20
|
/** Include foreign key differences */
|
|
21
21
|
checkForeignKeys?: boolean;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Include default value differences. Off by default: an engine reports a default as it stored it
|
|
24
|
+
* (`now()`, `CURRENT_TIMESTAMP`, `'active'::text`), which rarely matches the entity's literal.
|
|
25
|
+
*/
|
|
23
26
|
checkDefaults?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Tables to leave out of the comparison. The migrations bookkeeping table belongs here - it exists in
|
|
29
|
+
* the database by design and has no entity, so reporting it as unexpected told every project to
|
|
30
|
+
* "create entity or drop table" for its own migration log.
|
|
31
|
+
*/
|
|
32
|
+
excludeTables?: string[];
|
|
24
33
|
/** Dialect instance for type formatting */
|
|
25
34
|
dialect?: AbstractDialect;
|
|
26
35
|
}
|
|
@@ -22,6 +22,7 @@ export class DriftDetector {
|
|
|
22
22
|
checkIndexes: options.checkIndexes ?? true,
|
|
23
23
|
checkForeignKeys: options.checkForeignKeys ?? true,
|
|
24
24
|
checkDefaults: options.checkDefaults ?? false,
|
|
25
|
+
excludeTables: options.excludeTables ?? [],
|
|
25
26
|
dialect: options.dialect,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
@@ -30,7 +31,11 @@ export class DriftDetector {
|
|
|
30
31
|
*/
|
|
31
32
|
detect() {
|
|
32
33
|
const differ = new SchemaASTDiffer();
|
|
33
|
-
const diff = differ.diff(this.expectedAST, this.actualAST
|
|
34
|
+
const diff = differ.diff(this.expectedAST, this.actualAST, {
|
|
35
|
+
compareIndexes: this.options.checkIndexes,
|
|
36
|
+
compareRelationships: this.options.checkForeignKeys,
|
|
37
|
+
excludeTables: this.options.excludeTables,
|
|
38
|
+
});
|
|
34
39
|
const drifts = [
|
|
35
40
|
...this.detectTableDrifts(diff),
|
|
36
41
|
...this.detectColumnDrifts(diff),
|
|
@@ -137,13 +142,27 @@ export class DriftDetector {
|
|
|
137
142
|
});
|
|
138
143
|
}
|
|
139
144
|
}
|
|
145
|
+
if (this.options.checkDefaults && colDiff.expected && colDiff.actual) {
|
|
146
|
+
const expected = String(colDiff.expected.defaultValue ?? 'NULL');
|
|
147
|
+
const actual = String(colDiff.actual.defaultValue ?? 'NULL');
|
|
148
|
+
if (expected !== actual) {
|
|
149
|
+
drifts.push({
|
|
150
|
+
type: 'constraint_mismatch',
|
|
151
|
+
severity: 'info',
|
|
152
|
+
table: colDiff.table,
|
|
153
|
+
column: colDiff.column,
|
|
154
|
+
expected,
|
|
155
|
+
actual,
|
|
156
|
+
details: `Default mismatch for "${colDiff.column}"`,
|
|
157
|
+
suggestion: 'Align the default in the entity or the database',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
140
161
|
}
|
|
141
162
|
/**
|
|
142
163
|
* Detect index drifts.
|
|
143
164
|
*/
|
|
144
165
|
detectIndexDrifts(diff) {
|
|
145
|
-
if (!this.options.checkIndexes)
|
|
146
|
-
return [];
|
|
147
166
|
const drifts = [];
|
|
148
167
|
for (const idxDiff of diff.indexDiffs) {
|
|
149
168
|
if (idxDiff.type === 'create') {
|
|
@@ -173,8 +192,6 @@ export class DriftDetector {
|
|
|
173
192
|
* Detect relationship/FK drifts.
|
|
174
193
|
*/
|
|
175
194
|
detectRelationshipDrifts(diff) {
|
|
176
|
-
if (!this.options.checkForeignKeys)
|
|
177
|
-
return [];
|
|
178
195
|
const drifts = [];
|
|
179
196
|
for (const relDiff of diff.relationshipDiffs) {
|
|
180
197
|
if (relDiff.type === 'create') {
|
|
@@ -40,7 +40,11 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
|
|
|
40
40
|
getTableSchema(tableName: string): Promise<TableSchema | undefined>;
|
|
41
41
|
getTableNames(): Promise<string[]>;
|
|
42
42
|
tableExists(tableName: string): Promise<boolean>;
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Introspection reads, so `withQuerier` rather than `transaction`: the pool owns the release either
|
|
45
|
+
* way, and wrapping catalogue queries in a transaction would hold one open for nothing.
|
|
46
|
+
*/
|
|
47
|
+
protected withSqlQuerier<T>(task: (querier: SqlQuerier) => Promise<T>): Promise<T>;
|
|
44
48
|
protected tableExistsInternal(read: TableRowReader, tableName: string): Promise<boolean>;
|
|
45
49
|
protected getColumns(read: TableRowReader, tableName: string): Promise<ColumnSchema[]>;
|
|
46
50
|
protected getIndexes(read: TableRowReader, tableName: string): Promise<IndexSchema[]>;
|
|
@@ -29,9 +29,8 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
29
29
|
// Template Methods (shared control flow)
|
|
30
30
|
// ============================================================================
|
|
31
31
|
async getTableSchema(tableName) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
32
|
+
return this.withSqlQuerier(async (querier) => {
|
|
33
|
+
const read = createTableRowReader(querier);
|
|
35
34
|
const exists = await this.tableExistsInternal(read, tableName);
|
|
36
35
|
if (!exists) {
|
|
37
36
|
return undefined;
|
|
@@ -49,37 +48,28 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
49
48
|
indexes,
|
|
50
49
|
foreignKeys,
|
|
51
50
|
};
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
await querier.release();
|
|
55
|
-
}
|
|
51
|
+
});
|
|
56
52
|
}
|
|
57
53
|
async getTableNames() {
|
|
58
|
-
|
|
59
|
-
try {
|
|
54
|
+
return this.withSqlQuerier(async (querier) => {
|
|
60
55
|
const results = await querier.all(this.getTableNamesQuery());
|
|
61
56
|
return results.map((row) => this.mapTableNameRow(row));
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
await querier.release();
|
|
65
|
-
}
|
|
57
|
+
});
|
|
66
58
|
}
|
|
67
59
|
async tableExists(tableName) {
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
return this.tableExistsInternal(createTableRowReader(querier), tableName);
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
await querier.release();
|
|
74
|
-
}
|
|
60
|
+
return this.withSqlQuerier((querier) => this.tableExistsInternal(createTableRowReader(querier), tableName));
|
|
75
61
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Introspection reads, so `withQuerier` rather than `transaction`: the pool owns the release either
|
|
64
|
+
* way, and wrapping catalogue queries in a transaction would hold one open for nothing.
|
|
65
|
+
*/
|
|
66
|
+
withSqlQuerier(task) {
|
|
67
|
+
return this.pool.withQuerier((querier) => {
|
|
68
|
+
if (!isSqlQuerier(querier)) {
|
|
69
|
+
throw new Error(`${this.constructor.name} requires a SQL-based querier`);
|
|
70
|
+
}
|
|
71
|
+
return task(querier);
|
|
72
|
+
});
|
|
83
73
|
}
|
|
84
74
|
async tableExistsInternal(read, tableName) {
|
|
85
75
|
const results = await read(this.tableExistsQuery(), this.tableExistsParams(tableName));
|