uql-orm 0.22.0 → 0.24.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.
Files changed (107) hide show
  1. package/README.md +4 -4
  2. package/dist/browser/uql-browser.min.js.map +3 -3
  3. package/dist/dialect/abstractSqlDialect.d.ts +14 -1
  4. package/dist/dialect/abstractSqlDialect.js +21 -14
  5. package/dist/dialect/mysqlLikeSqlDialect.d.ts +11 -1
  6. package/dist/dialect/mysqlLikeSqlDialect.js +25 -0
  7. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -7
  8. package/dist/dialect/pgLikeSqlDialect.js +7 -12
  9. package/dist/entity/decorator/bag.d.ts +35 -0
  10. package/dist/entity/decorator/bag.js +54 -0
  11. package/dist/entity/decorator/entity.d.ts +28 -2
  12. package/dist/entity/decorator/entity.js +40 -2
  13. package/dist/entity/decorator/members.d.ts +51 -0
  14. package/dist/entity/decorator/members.js +51 -0
  15. package/dist/entity/index.d.ts +3 -1
  16. package/dist/entity/index.js +3 -1
  17. package/dist/entity/metadata/definition.d.ts +23 -9
  18. package/dist/entity/metadata/definition.js +55 -100
  19. package/dist/http/contract.js +1 -2
  20. package/dist/http/handler.js +5 -26
  21. package/dist/http/query.js +1 -1
  22. package/dist/migrate/acquireQuerierForMigrations.d.ts +13 -1
  23. package/dist/migrate/acquireQuerierForMigrations.js +28 -0
  24. package/dist/migrate/cli-config.js +24 -8
  25. package/dist/migrate/cli.js +5 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +3 -4
  27. package/dist/migrate/codegen/index.d.ts +0 -1
  28. package/dist/migrate/codegen/index.js +0 -2
  29. package/dist/migrate/drift/driftDetector.d.ts +10 -1
  30. package/dist/migrate/drift/driftDetector.js +22 -5
  31. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -1
  32. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +17 -27
  33. package/dist/migrate/introspection/mongoIntrospector.js +7 -13
  34. package/dist/migrate/migrator.js +44 -81
  35. package/dist/migrate/storage/databaseStorage.d.ts +2 -0
  36. package/dist/migrate/storage/databaseStorage.js +8 -23
  37. package/dist/mongo/mongodbQuerier.js +171 -229
  38. package/dist/nestjs/uqlContextInterceptor.js +58 -22
  39. package/dist/nestjs/uqlModule.d.ts +2 -11
  40. package/dist/nestjs/uqlModule.js +93 -44
  41. package/dist/postgres/postgresDialect.js +2 -2
  42. package/dist/querier/abstractQuerier.d.ts +28 -0
  43. package/dist/querier/abstractQuerier.js +47 -22
  44. package/dist/querier/abstractQuerierPool.d.ts +21 -1
  45. package/dist/querier/abstractQuerierPool.js +113 -3
  46. package/dist/querier/abstractSqlQuerier.d.ts +1 -3
  47. package/dist/querier/abstractSqlQuerier.js +51 -96
  48. package/dist/querier/index.d.ts +3 -1
  49. package/dist/querier/index.js +3 -1
  50. package/dist/querier/querierContext.browser.d.ts +12 -0
  51. package/dist/querier/querierContext.browser.js +18 -0
  52. package/dist/querier/querierContext.d.ts +22 -0
  53. package/dist/querier/querierContext.js +42 -0
  54. package/dist/querier/queryError.d.ts +22 -0
  55. package/dist/querier/queryError.js +20 -0
  56. package/dist/querier/transactional.d.ts +26 -0
  57. package/dist/querier/transactional.js +43 -0
  58. package/dist/schema/index.d.ts +1 -1
  59. package/dist/schema/schemaASTBuilder.d.ts +1 -1
  60. package/dist/schema/schemaASTBuilder.js +2 -2
  61. package/dist/schema/types.d.ts +0 -21
  62. package/dist/sqlite/abstractSqliteQuerier.d.ts +9 -2
  63. package/dist/sqlite/hranaQuerier.js +20 -42
  64. package/dist/sqlite/index.d.ts +1 -0
  65. package/dist/sqlite/index.js +1 -0
  66. package/dist/sqlite/localSqliteQuerierPool.d.ts +31 -0
  67. package/dist/sqlite/localSqliteQuerierPool.js +34 -0
  68. package/dist/sqlite/nodeSqliteAdapter.d.ts +35 -0
  69. package/dist/sqlite/nodeSqliteAdapter.js +28 -0
  70. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +28 -0
  71. package/dist/sqlite/nodeSqliteQuerierPool.js +29 -0
  72. package/dist/sqlite/sqliteDialect.d.ts +1 -2
  73. package/dist/sqlite/sqliteDialect.js +0 -13
  74. package/dist/sqlite/sqliteQuerierPool.d.ts +10 -23
  75. package/dist/sqlite/sqliteQuerierPool.js +9 -28
  76. package/dist/type/entity.d.ts +95 -28
  77. package/dist/type/querier.d.ts +8 -35
  78. package/dist/type/querierPool.d.ts +13 -12
  79. package/dist/type/universalQuerier.d.ts +23 -7
  80. package/dist/util/hook.util.js +1 -1
  81. package/package.json +7 -16
  82. package/dist/entity/decorator/field.d.ts +0 -2
  83. package/dist/entity/decorator/field.js +0 -7
  84. package/dist/entity/decorator/filter.d.ts +0 -7
  85. package/dist/entity/decorator/filter.js +0 -11
  86. package/dist/entity/decorator/hook.d.ts +0 -7
  87. package/dist/entity/decorator/hook.js +0 -14
  88. package/dist/entity/decorator/id.d.ts +0 -2
  89. package/dist/entity/decorator/id.js +0 -7
  90. package/dist/entity/decorator/index-decorator.d.ts +0 -22
  91. package/dist/entity/decorator/index-decorator.js +0 -26
  92. package/dist/entity/decorator/index.d.ts +0 -8
  93. package/dist/entity/decorator/index.js +0 -8
  94. package/dist/entity/decorator/relation.d.ts +0 -8
  95. package/dist/entity/decorator/relation.js +0 -19
  96. package/dist/migrate/codegen/smartRelationDetector.d.ts +0 -47
  97. package/dist/migrate/codegen/smartRelationDetector.js +0 -134
  98. package/dist/querier/decorator/index.d.ts +0 -4
  99. package/dist/querier/decorator/index.js +0 -4
  100. package/dist/querier/decorator/injectQuerier.d.ts +0 -3
  101. package/dist/querier/decorator/injectQuerier.js +0 -33
  102. package/dist/querier/decorator/log.d.ts +0 -32
  103. package/dist/querier/decorator/log.js +0 -52
  104. package/dist/querier/decorator/serialized.d.ts +0 -6
  105. package/dist/querier/decorator/serialized.js +0 -13
  106. package/dist/querier/decorator/transactional.d.ts +0 -6
  107. package/dist/querier/decorator/transactional.js +0 -48
@@ -1,17 +1,31 @@
1
- import type { EntityIndexInput, EntityMeta, EntityOptions, FieldOptions, FilterOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
2
- /**
3
- * Append a composite index entry with normalized `unique` (default false) and columns. Normalizing
4
- * here is what lets the dialects render one shape instead of re-parsing the authored sugar.
5
- */
6
- export declare function appendEntityIndex<E>(meta: EntityMeta<E>, index: EntityIndexInput): void;
1
+ import type { EntityIndexInput, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
7
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
8
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
9
- export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions<E>): EntityMeta<E>;
4
+ export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
10
5
  export declare function defineHook<E>(entity: Type<E>, methodName: string, event: HookEvent): EntityMeta<E>;
11
- export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput): EntityMeta<E>;
6
+ /**
7
+ * Declares a composite index. `unique` and the authored column sugar are normalized here, which is what
8
+ * lets the dialects render one shape instead of re-parsing it.
9
+ */
10
+ export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E>>): EntityMeta<E>;
12
11
  export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
12
+ /**
13
+ * What a decorator bag and {@link EntityOptions} have in common at registration time. The keyed mapped
14
+ * types in `EntityOptions<E>` are what check the imperative call; a member decorator has no class to key
15
+ * against, so by the time either reaches the primitives the keys are plain strings.
16
+ */
17
+ type MemberSpecs = {
18
+ readonly fields?: Readonly<Record<string, FieldOptions | undefined>>;
19
+ readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
20
+ readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
21
+ };
22
+ /**
23
+ * Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
24
+ * API converge on one registration path before anything is finalized.
25
+ */
26
+ export declare function applyMembers<E>(entity: Type<E>, specs: MemberSpecs | undefined): void;
13
27
  export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
14
28
  export declare function getEntities(): Type<unknown>[];
15
29
  export declare function ensureMeta<E>(entity: Type<E>): EntityMeta<E>;
16
30
  export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
17
- export declare function isValidEntityType(type: unknown): type is Type<unknown>;
31
+ export {};
@@ -1,52 +1,45 @@
1
1
  import { getKeys, hasKeys, lowerFirst, normalizeIndexColumn, upperFirst } from '../../util/index.js';
2
- import { LoggerWrapper } from '../../util/logger.js';
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
- opts = { ...opts, typeInferred: false };
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
- meta.fields[fieldKey] = { ...meta.fields[fieldKey], ...{ name: key, ...opts } };
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
- registrationLogger.logInfo(`Overriding ID property for '${entity.name}' from '${id}' to '${key}'`);
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
- const resolved = opts.entity
42
- ? opts
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], ...resolved };
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
- appendEntityIndex(meta, index);
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
- function applyBulkFields(entity, fields) {
80
- for (const key of Object.keys(fields)) {
81
- const spec = fields[key];
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
- function applyBulkIndexes(entity, indexes) {
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 applyBulkFilters(entity, filters) {
116
- for (const name of Object.keys(filters)) {
117
- const spec = filters[name];
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 parentMeta = ensureMeta(proto.constructor);
148
- extendMeta(meta, parentMeta);
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
- typeInferred: true,
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
- }
@@ -1,5 +1,4 @@
1
- // import from the specific util modules (not the barrel) to keep entity
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
  /**
@@ -1,6 +1,6 @@
1
1
  import { withContext } from '../context/context.js';
2
2
  import { getEntities, getMeta } from '../entity/index.js';
3
- import { getQuerier } from '../options.js';
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
- async function withQuerier(fn) {
139
- const querier = await getQuerier();
140
- try {
141
- return await fn(querier);
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;
@@ -1,4 +1,4 @@
1
- // specific util module (not the barrel) to keep reflect-metadata out of the browser bundle
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 { Querier, QuerierPool } from '../type/index.js';
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
- /** `jiti` (1.8 MB) makes `uql.config.ts` loadable; imported here so only projects with one pay for it. */
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 { createJiti } = await import('jiti').catch(() => {
6
- throw new Error("Loading a uql config file requires 'jiti'. Run `npm i -D jiti`, or pass the config inline instead of via a config file.");
7
- });
8
- return createJiti(process.cwd()).import(path, { default: true });
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 Error(`Could not find uql configuration file at ${customPath}`);
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 Error(`Could not load configuration file at ${customPath}: ${error.message}`);
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 Error('Could not find uql configuration file. Create a uql.config.ts or uql.config.js file in your project root.');
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
  }
@@ -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, { dialect: config.pool?.dialect });
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}?: Relation<${relatedClassName}>;`);
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}?: Relation<${relatedClassName}[]>;`);
264
+ lines.push(` ${propertyName}?: ${relatedClassName}[];`);
266
265
  }
267
266
  else {
268
- lines.push(` ${propertyName}?: Relation<${relatedClassName}>;`);
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
- /** Include default value differences */
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
- protected getQuerier(): Promise<SqlQuerier>;
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[]>;