uql-orm 0.56.0 → 0.58.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 (110) hide show
  1. package/README.md +7 -9
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  5. package/dist/cockroachdb/cockroachDialect.js +2 -10
  6. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  7. package/dist/d1/d1SqliteDialect.js +2 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +196 -33
  9. package/dist/dialect/abstractSqlDialect.js +410 -203
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +12 -7
  12. package/dist/dialect/hydrateColumn.d.ts +8 -2
  13. package/dist/dialect/hydrateColumn.js +33 -1
  14. package/dist/dialect/jsonSql.d.ts +13 -5
  15. package/dist/dialect/jsonSql.js +24 -7
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
  17. package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  19. package/dist/dialect/pgLikeSqlDialect.js +23 -48
  20. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  21. package/dist/dialect/pgVectorMetrics.js +17 -0
  22. package/dist/dialect/queryContext.d.ts +3 -7
  23. package/dist/dialect/queryContext.js +13 -8
  24. package/dist/dialect/queryJoins.d.ts +8 -4
  25. package/dist/dialect/queryJoins.js +26 -11
  26. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  27. package/dist/dialect/vectorSqlDialect.js +2 -3
  28. package/dist/entity/decorator/bag.d.ts +2 -2
  29. package/dist/entity/decorator/entity.d.ts +8 -9
  30. package/dist/entity/decorator/entity.js +6 -7
  31. package/dist/entity/decorator/members.d.ts +7 -6
  32. package/dist/entity/decorator/members.js +2 -1
  33. package/dist/entity/metadata/definition.d.ts +16 -11
  34. package/dist/entity/metadata/definition.js +54 -42
  35. package/dist/http/handler.d.ts +2 -2
  36. package/dist/http/handler.js +0 -1
  37. package/dist/maria/mariaDialect.d.ts +13 -6
  38. package/dist/maria/mariaDialect.js +29 -9
  39. package/dist/migrate/cli.d.ts +2 -3
  40. package/dist/migrate/cli.js +2 -2
  41. package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
  42. package/dist/migrate/codegen/entityTypes.d.ts +1 -1
  43. package/dist/migrate/codegen/entityTypes.js +4 -3
  44. package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
  45. package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
  46. package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
  47. package/dist/migrate/codegen/sourceLiteral.js +4 -0
  48. package/dist/migrate/ddl/index.d.ts +1 -5
  49. package/dist/migrate/ddl/index.js +14 -25
  50. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  51. package/dist/migrate/ddl/indexDdl.js +17 -1
  52. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  53. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  54. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  55. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  56. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
  57. package/dist/migrate/ddl/pgIndexDdl.js +29 -12
  58. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
  59. package/dist/migrate/migrator.d.ts +4 -4
  60. package/dist/migrate/schemaGenerator.d.ts +8 -8
  61. package/dist/migrate/schemaGenerator.js +5 -7
  62. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  63. package/dist/mongo/mongoDialect.d.ts +31 -18
  64. package/dist/mongo/mongoDialect.js +146 -104
  65. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  66. package/dist/mongo/mongodbQuerier.js +31 -106
  67. package/dist/mssql/mssqlDialect.d.ts +16 -0
  68. package/dist/mssql/mssqlDialect.js +26 -4
  69. package/dist/mysql/mysqlDialect.d.ts +2 -0
  70. package/dist/mysql/mysqlDialect.js +4 -0
  71. package/dist/querier/abstractQuerier.d.ts +20 -36
  72. package/dist/querier/abstractQuerier.js +35 -129
  73. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  74. package/dist/querier/abstractSqlQuerier.d.ts +4 -17
  75. package/dist/querier/abstractSqlQuerier.js +40 -50
  76. package/dist/schema/canonicalType.js +4 -4
  77. package/dist/schema/indexDifferences.js +4 -4
  78. package/dist/schema/schemaASTBuilder.d.ts +3 -3
  79. package/dist/schema/schemaASTBuilder.js +32 -3
  80. package/dist/schema/schemaASTDiffer.js +5 -5
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +38 -7
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +3 -3
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +110 -69
  88. package/dist/type/migration.d.ts +7 -7
  89. package/dist/type/migratorDialect.d.ts +4 -0
  90. package/dist/type/querier.d.ts +6 -6
  91. package/dist/type/querierPool.d.ts +2 -2
  92. package/dist/type/query.d.ts +41 -72
  93. package/dist/type/query.js +10 -5
  94. package/dist/type/queryAggregate.d.ts +43 -34
  95. package/dist/type/queryAggregate.js +1 -1
  96. package/dist/type/queryWhere.d.ts +12 -9
  97. package/dist/type/universalQuerier.d.ts +4 -4
  98. package/dist/util/dialect.util.d.ts +4 -4
  99. package/dist/util/dialect.util.js +24 -15
  100. package/dist/util/field.util.d.ts +5 -0
  101. package/dist/util/field.util.js +19 -0
  102. package/dist/util/object.util.d.ts +2 -0
  103. package/dist/util/object.util.js +4 -0
  104. package/dist/util/relationQuery.util.d.ts +12 -65
  105. package/dist/util/relationQuery.util.js +27 -81
  106. package/dist/util/rowKey.util.d.ts +1 -11
  107. package/dist/util/rowKey.util.js +1 -13
  108. package/package.json +1 -1
  109. package/dist/querier/relationCount.d.ts +0 -16
  110. package/dist/querier/relationCount.js +0 -121
@@ -1,5 +1,4 @@
1
- import type { EntityMeta, Query, QuerySortMap, RelationMeta, Type } from '../type/index.js';
2
- import { type RelationQuery } from '../util/index.js';
1
+ import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
3
2
  /**
4
3
  * One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
5
4
  * `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
@@ -8,8 +7,10 @@ import { type RelationQuery } from '../util/index.js';
8
7
  export type QueryJoin = {
9
8
  /** The relation key on its parent, which is how MongoDB names the field a `$lookup` adds. */
10
9
  readonly key: string;
11
- /** Dotted path from the queried entity, which is how the SQL dialects alias the join. */
10
+ /** Dotted path from the queried entity, which is what a joined row's columns answer under. */
12
11
  readonly path: string;
12
+ /** The alias the statement reads it through: its path, unless another table of the statement took it. */
13
+ readonly alias: string;
13
14
  readonly entity: Type<object>;
14
15
  readonly meta: EntityMeta<object>;
15
16
  readonly relation: RelationMeta;
@@ -38,14 +39,17 @@ export type QuerySortOptions = {
38
39
  * related column needs that relation joined just as much as selecting it does. The two sources meet
39
40
  * here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
40
41
  * statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
42
+ * `claimAlias` names each join's table, parents first.
41
43
  */
42
- export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>): QueryJoins;
44
+ export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
43
45
  /**
44
46
  * Whether a join drops parents that have no match, which is the one thing a join does to *how many*
45
47
  * rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
46
48
  * it counts the parents the read will never hand back.
47
49
  */
48
50
  export declare function hasRequiredJoin<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
51
+ /** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
52
+ export declare function aggregatesRelations<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
49
53
  /**
50
54
  * The join an ordering may address at `path`, with the relation's own sort map, or why it may not.
51
55
  * Every backend answers this the same way - a to-many has no single value to order by, a relation
@@ -1,19 +1,20 @@
1
1
  import { getMeta, relationOf } from '../entity/index.js';
2
- import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey, } from '../util/index.js';
2
+ import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey } from '../util/index.js';
3
3
  export const NO_JOINS = new Map();
4
4
  /**
5
5
  * What the statement joins, from the whole query rather than from `$populate` alone: ordering by a
6
6
  * related column needs that relation joined just as much as selecting it does. The two sources meet
7
7
  * here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
8
8
  * statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
9
+ * `claimAlias` names each join's table, parents first.
9
10
  */
10
- export function resolveQueryJoins(meta, q) {
11
+ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
11
12
  if (!q.$populate && !q.$sort) {
12
13
  return NO_JOINS;
13
14
  }
14
15
  const joins = new Map();
15
- addPopulateJoins(joins, meta, q.$populate);
16
- addSortJoins(joins, meta, q.$sort);
16
+ addPopulateJoins(joins, claimAlias, meta, q.$populate);
17
+ addSortJoins(joins, claimAlias, meta, q.$sort);
17
18
  return joins;
18
19
  }
19
20
  /**
@@ -29,7 +30,19 @@ export function hasRequiredJoin(meta, q) {
29
30
  }
30
31
  return false;
31
32
  }
32
- function addJoin(joins, parent, key, relation, query, required, projected) {
33
+ /** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
34
+ export function aggregatesRelations(meta, q) {
35
+ if (getRelationRequestSummary(meta, q.$populate).toManyKeys.length) {
36
+ return true;
37
+ }
38
+ for (const join of resolveQueryJoins(meta, q).values()) {
39
+ if (getRelationRequestSummary(join.meta, join.query.$populate).toManyKeys.length) {
40
+ return true;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ function addJoin(joins, claimAlias, parent, key, relation, query, required, projected) {
33
46
  const path = parent ? `${parent.path}.${key}` : key;
34
47
  const existing = joins.get(path);
35
48
  // `$populate` runs first, so an already-joined relation keeps its columns and its `$required`
@@ -41,6 +54,7 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
41
54
  const join = {
42
55
  key,
43
56
  path,
57
+ alias: claimAlias(path),
44
58
  entity,
45
59
  meta: getMeta(entity),
46
60
  relation,
@@ -52,15 +66,15 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
52
66
  joins.set(path, join);
53
67
  return join;
54
68
  }
55
- function addPopulateJoins(joins, meta, populate, parent) {
69
+ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
56
70
  for (const key of getRelationRequestSummary(meta, populate).joinableKeys) {
57
71
  const relation = relationOf(meta, key);
58
72
  const { query, required } = parseRelationAtKey(key, populate);
59
- const join = addJoin(joins, parent, key, relation, query, required, true);
60
- addPopulateJoins(joins, join.meta, query.$populate, join);
73
+ const join = addJoin(joins, claimAlias, parent, key, relation, query, required, true);
74
+ addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
61
75
  }
62
76
  }
63
- function addSortJoins(joins, meta, sort, parent) {
77
+ function addSortJoins(joins, claimAlias, meta, sort, parent) {
64
78
  if (!sort) {
65
79
  return;
66
80
  }
@@ -72,8 +86,9 @@ function addSortJoins(joins, meta, sort, parent) {
72
86
  if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
73
87
  continue;
74
88
  }
75
- const join = addJoin(joins, parent, key, relation, {}, false, false);
76
- addSortJoins(joins, join.meta, value, join);
89
+ const join = addJoin(joins, claimAlias, parent, key, relation, {}, false, false);
90
+ // `E` stated: inferred from a `QuerySortMap<object>`, it lands on the nested relation's target.
91
+ addSortJoins(joins, claimAlias, join.meta, value, join);
77
92
  }
78
93
  }
79
94
  /**
@@ -78,8 +78,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
78
78
  */
79
79
  supportedVectorType(cast: VectorCast): VectorCast;
80
80
  /**
81
- * Append a vector distance projection.
82
- * Delegates to `appendVectorSort` so each dialect's distance syntax is written once.
81
+ * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
82
+ * `appendVectorSort` so each dialect's distance syntax is written once.
83
83
  */
84
84
  protected appendVectorProjection<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
85
85
  /**
@@ -96,8 +96,8 @@ export class VectorSqlDialect extends AbstractDialect {
96
96
  return this.hasNarrowVectorTypes ? cast : 'vector';
97
97
  }
98
98
  /**
99
- * Append a vector distance projection.
100
- * Delegates to `appendVectorSort` so each dialect's distance syntax is written once.
99
+ * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
100
+ * `appendVectorSort` so each dialect's distance syntax is written once.
101
101
  */
102
102
  appendVectorProjection(ctx, meta, key, search) {
103
103
  const alias = search.$project;
@@ -108,7 +108,6 @@ export class VectorSqlDialect extends AbstractDialect {
108
108
  throw new TypeError(`$project '${alias}' collides with a field of '${entityName(meta)}'`);
109
109
  }
110
110
  this.appendVectorSort(ctx, meta, key, search);
111
- ctx.append(` AS ${this.escapeId(alias)}`);
112
111
  }
113
112
  /**
114
113
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
@@ -1,4 +1,4 @@
1
- import type { FieldOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
1
+ import type { FieldOptions, HookEvent, RelationRegistration, Type } from '../../type/index.js';
2
2
  /**
3
3
  * What the member decorators record for one class, waiting for `@Entity()` or `defineEntity` to drain
4
4
  * it into the metadata registry. Member decorators receive no class reference under the standard
@@ -9,7 +9,7 @@ import type { FieldOptions, HookEvent, RelationOptions, Type } from '../../type/
9
9
  */
10
10
  export type MemberRegistrations = {
11
11
  readonly fields: Record<string, FieldOptions>;
12
- readonly relations: Record<string, RelationOptions>;
12
+ readonly relations: Record<string, RelationRegistration>;
13
13
  readonly hooks: Partial<Record<HookEvent, string[]>>;
14
14
  };
15
15
  /**
@@ -1,4 +1,4 @@
1
- import type { EntityOptions, FieldKey, FilterOptions, IndexColumnInput, IndexOptions, Type } from '../../type/index.js';
1
+ import type { EntityIndexOptions, EntityOptions, FieldKey, FilterOptions, IndexColumnInput, KeyMap, Type } from '../../type/index.js';
2
2
  /**
3
3
  * Marks a class as an entity and finalizes its metadata.
4
4
  *
@@ -16,13 +16,12 @@ export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>, co
16
16
  */
17
17
  export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity: Type<E>) => void;
18
18
  /**
19
- * Declares a composite index. Stacks, so several may sit above one class.
19
+ * Declares a composite index, its columns read off the key map. Stacks, so several may sit above one
20
+ * class. `E` is inferred from the class the returned decorator is applied to, which is what types the
21
+ * key map: `@Index((user) => [user.nope])` does not compile, and a rename reaches every column.
20
22
  *
21
- * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
22
- * names be checked against it: `@Index(['nope'])` does not compile.
23
- *
24
- * @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
25
- * @example `@Index(['email'], { unique: true })`
26
- * @example `@Index(['status'], { where: "status = 'active'" })`
23
+ * @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
24
+ * @example `@Index((user) => [user.email], { unique: true })`
25
+ * @example `@Index((user) => [user.status], { where: "status = 'active'" })`
27
26
  */
28
- export declare function Index<E>(columns: readonly IndexColumnInput<FieldKey<E>, E>[], options?: IndexOptions<E>): (entity: Type<E>) => void;
27
+ export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<NoInfer<E>>, NoInfer<E>>[], options?: EntityIndexOptions<NoInfer<E>>): (entity: Type<E>) => void;
@@ -28,14 +28,13 @@ export function Filter(name, opts) {
28
28
  };
29
29
  }
30
30
  /**
31
- * Declares a composite index. Stacks, so several may sit above one class.
31
+ * Declares a composite index, its columns read off the key map. Stacks, so several may sit above one
32
+ * class. `E` is inferred from the class the returned decorator is applied to, which is what types the
33
+ * key map: `@Index((user) => [user.nope])` does not compile, and a rename reaches every column.
32
34
  *
33
- * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
34
- * names be checked against it: `@Index(['nope'])` does not compile.
35
- *
36
- * @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
37
- * @example `@Index(['email'], { unique: true })`
38
- * @example `@Index(['status'], { where: "status = 'active'" })`
35
+ * @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
36
+ * @example `@Index((user) => [user.email], { unique: true })`
37
+ * @example `@Index((user) => [user.status], { where: "status = 'active'" })`
39
38
  */
40
39
  export function Index(columns, options = {}) {
41
40
  return (entity) => {
@@ -1,7 +1,7 @@
1
1
  import type { EntityGetter, FieldOptions, FieldType, IdValue, NamedIdKey, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
2
2
  import type { RejectIncompatible } from '../../util/index.js';
3
- /** A member decorator that also constrains the property it may be applied to. */
4
- type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<unknown, V>) => void;
3
+ /** A member decorator that also constrains the property it may be applied to, on a class `O`. */
4
+ type MemberDecorator<V, O = unknown> = (value: undefined, context: ClassFieldDecoratorContext<O, V>) => void;
5
5
  /**
6
6
  * Maps any option the type does not declare to `never`, turning a typo into a compile error.
7
7
  *
@@ -73,14 +73,15 @@ export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
73
73
  * `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
74
74
  * entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
75
75
  * cardinality on a non-array property does too. `entity` is required because nothing reflects it now.
76
+ * `O` is inferred from the class the decorator sits on, which types `references`' own side.
76
77
  */
77
78
  type WithEntity<E, O> = O & {
78
79
  readonly entity: EntityGetter<E>;
79
80
  };
80
- export declare function OneToOne<E>(opts: WithEntity<E, RelationOneToOneOptions<E>>): MemberDecorator<E | undefined>;
81
- export declare function ManyToOne<E>(opts: WithEntity<E, RelationManyToOneOptions<E>>): MemberDecorator<E | undefined>;
82
- export declare function OneToMany<E>(opts: WithEntity<E, RelationOneToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
83
- export declare function ManyToMany<E>(opts: WithEntity<E, RelationManyToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
81
+ export declare function OneToOne<E extends object, O>(opts: WithEntity<E, RelationOneToOneOptions<E, O>>): MemberDecorator<E | undefined, O>;
82
+ export declare function ManyToOne<E extends object, O>(opts: WithEntity<E, RelationManyToOneOptions<E, O>>): MemberDecorator<E | undefined, O>;
83
+ export declare function OneToMany<E extends object, O>(opts: WithEntity<E, RelationOneToManyOptions<E, O>>): MemberDecorator<readonly E[] | undefined, O>;
84
+ export declare function ManyToMany<E extends object, O>(opts: WithEntity<E, RelationManyToManyOptions<E, O>>): MemberDecorator<readonly E[] | undefined, O>;
84
85
  export declare const BeforeInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
85
86
  export declare const AfterInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
86
87
  export declare const BeforeUpdate: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
@@ -1,3 +1,4 @@
1
+ import { relationRegistration } from '../metadata/definition.js';
1
2
  import { memberRegistrations } from './bag.js';
2
3
  /**
3
4
  * Declares a persisted field.
@@ -28,7 +29,7 @@ export function Id(opts) {
28
29
  }
29
30
  function relation(opts) {
30
31
  return (_value, context) => {
31
- memberRegistrations(context.metadata).relations[String(context.name)] = opts;
32
+ memberRegistrations(context.metadata).relations[String(context.name)] = relationRegistration(opts);
32
33
  };
33
34
  }
34
35
  export function OneToOne(opts) {
@@ -1,13 +1,20 @@
1
- import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldKey, FieldMeta, FieldOptions, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, Type, WrittenId } from '../../type/index.js';
1
+ import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
2
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
3
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
4
- export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
4
+ /** `T` is the relation's target, independent of the owner `E`. */
5
+ export declare function defineRelation<E, T extends object>(entity: Type<E>, key: string, opts: RelationOptions<T, E>): EntityMeta<E>;
6
+ /**
7
+ * `opts` as the registry takes them: `mappedBy` and `references` read off their key maps down to the
8
+ * names they give. The callbacks only read properties, so they run here, before any entity has to
9
+ * exist, and the registry holds data alone.
10
+ */
11
+ export declare function relationRegistration<T extends object, O>({ mappedBy, references, ...opts }: RelationOptions<T, O>): RelationRegistration;
5
12
  export declare function defineHook<E>(entity: Type<E>, methodName: string, event: HookEvent): EntityMeta<E>;
6
13
  /**
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.
14
+ * Declares a composite index, its columns read off the key map. `unique` and the authored column sugar
15
+ * are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
9
16
  */
10
- export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<FieldKey<E>, E>): EntityMeta<E>;
17
+ export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<E>): EntityMeta<E>;
11
18
  export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
12
19
  /**
13
20
  * Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
@@ -15,10 +22,8 @@ export declare function defineFilter<E>(entity: Type<E>, name: string, opts: Fil
15
22
  */
16
23
  export declare function applyMembers<E>(entity: Type<E>, specs: EntityMembers | undefined): void;
17
24
  /**
18
- * Registers an entity described by data alone, minting the class the registry keys it by. The row
19
- * type follows from the spec - see {@link SpecRow} - so a definition written out is checked column by
20
- * column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
21
- * cannot describe, such as the interface `uql-migrate types` generated for it.
25
+ * Registers a class as an entity from `opts` alone, the decorator-free counterpart of `@Entity()` with
26
+ * `@Field`/`@ManyToOne`/...
22
27
  */
23
28
  export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
24
29
  /**
@@ -31,7 +36,7 @@ export declare function assertSoleId<E>(meta: EntityMeta<E>, what: string): void
31
36
  /** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
32
37
  export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>;
33
38
  /** The field `key` names, for a caller that took `key` from the metadata itself. */
34
- export declare function fieldOf<E>(meta: EntityMeta<E>, key: FieldKey<E>): FieldMeta;
39
+ export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
35
40
  /** The relation `key` names, for a caller that took `key` from the metadata itself. */
36
41
  export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
37
42
  /**
@@ -61,5 +66,5 @@ export declare function idOf<E>(meta: EntityMeta<E>, row: EntityData<E>): Writte
61
66
  * registration). See the Runtime Schemas guide.
62
67
  */
63
68
  export declare function removeEntity<E>(entity: Type<E>): boolean;
64
- export declare function getEntities(): Type<unknown>[];
69
+ export declare function getEntities(): Type<object>[];
65
70
  export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
@@ -38,18 +38,41 @@ export function defineField(entity, key, opts = {}) {
38
38
  export function defineId(entity, key, opts) {
39
39
  return defineField(entity, key, { ...opts, isId: true });
40
40
  }
41
- // `RelationOptions` is parameterized by the *target* entity, which is independent of the owner `E`, so it
42
- // is left at its default here rather than tied to the class being registered.
41
+ /** `T` is the relation's target, independent of the owner `E`. */
43
42
  export function defineRelation(entity, key, opts) {
44
- if (!opts.entity) {
43
+ return addRelation(entity, key, relationRegistration(opts));
44
+ }
45
+ /**
46
+ * `opts` as the registry takes them: `mappedBy` and `references` read off their key maps down to the
47
+ * names they give. The callbacks only read properties, so they run here, before any entity has to
48
+ * exist, and the registry holds data alone.
49
+ */
50
+ export function relationRegistration({ mappedBy, references, ...opts }) {
51
+ return {
52
+ ...opts,
53
+ ...(mappedBy ? { mappedBy: mappedBy(keyMap()) } : {}),
54
+ ...(references ? { references: [...references(keyMap(), keyMap())] } : {}),
55
+ };
56
+ }
57
+ /** Every entity's key map: a callback only reads one property off it, and that property is its own key. */
58
+ function keyMap() {
59
+ return KEY_MAP;
60
+ }
61
+ const KEY_MAP = new Proxy({}, { get: (_, key) => key });
62
+ function addRelation(entity, key, registration) {
63
+ if (!registration.entity) {
45
64
  throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
46
65
  }
66
+ if (registration.through && registration.references) {
67
+ throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose columns follow the convention; 'references' ` +
68
+ "pairs the declaring entity's columns with the target's instead.");
69
+ }
47
70
  const meta = ensureWritableMeta(entity);
48
- // Registration writes the authored shape into a map declared as resolved: `getMeta` runs
49
- // `fillRelations`, which settles `entity`, `references` and `mappedBy` or throws. Bridging the two
50
- // shapes here is what lets every consumer read `RelationMeta` without asserting.
71
+ // Registration writes into a map declared as resolved: `getMeta` runs `fillRelations`, which settles
72
+ // `references` or throws. Bridging the two shapes here is what lets every consumer read `RelationMeta`
73
+ // without asserting.
51
74
  const relations = meta.relations;
52
- relations[key] = { ...relations[key], ...opts };
75
+ relations[key] = { ...relations[key], ...registration };
53
76
  return meta;
54
77
  }
55
78
  export function defineHook(entity, methodName, event) {
@@ -62,18 +85,18 @@ export function defineHook(entity, methodName, event) {
62
85
  return meta;
63
86
  }
64
87
  /**
65
- * Declares a composite index. `unique` and the authored column sugar are normalized here, which is what
66
- * lets the dialects render one shape instead of re-parsing it.
88
+ * Declares a composite index, its columns read off the key map. `unique` and the authored column sugar
89
+ * are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
67
90
  */
68
91
  export function defineIndex(entity, index) {
69
92
  const meta = ensureWritableMeta(entity);
70
- if (!meta.indexes)
71
- meta.indexes = [];
72
- meta.indexes.push({
93
+ const keys = keyMap();
94
+ (meta.indexes ??= []).push({
73
95
  ...index,
74
96
  unique: index.unique ?? false,
75
97
  where: ddlText(index.where, 'a partial-index predicate'),
76
- columns: index.columns.map(normalizeIndexColumn),
98
+ columns: index.columns(keys).map(normalizeIndexColumn),
99
+ include: index.include?.(keys),
77
100
  });
78
101
  return meta;
79
102
  }
@@ -104,7 +127,7 @@ export function applyMembers(entity, specs) {
104
127
  }
105
128
  }
106
129
  for (const [key, spec] of definedEntries(specs?.relations ?? {})) {
107
- defineRelation(entity, key, spec);
130
+ addRelation(entity, key, spec);
108
131
  }
109
132
  for (const [event, methodNames] of definedEntries(specs?.hooks ?? {})) {
110
133
  for (const methodName of methodNames) {
@@ -113,10 +136,8 @@ export function applyMembers(entity, specs) {
113
136
  }
114
137
  }
115
138
  /**
116
- * Registers an entity described by data alone, minting the class the registry keys it by. The row
117
- * type follows from the spec - see {@link SpecRow} - so a definition written out is checked column by
118
- * column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
119
- * cannot describe, such as the interface `uql-migrate types` generated for it.
139
+ * Registers a class as an entity from `opts` alone, the decorator-free counterpart of `@Entity()` with
140
+ * `@Field`/`@ManyToOne`/...
120
141
  */
121
142
  export function defineEntity(entity, opts = {}) {
122
143
  // Ahead of any registration, so a rejected definition leaves nothing half-written in the registry.
@@ -132,7 +153,12 @@ export function defineEntity(entity, opts = {}) {
132
153
  // drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
133
154
  // after class decorators return; draining empties the bag, so whichever runs second is a no-op.
134
155
  applyMembers(entity, ownRegistrations(entity));
135
- applyMembers(entity, opts);
156
+ const keys = keyMap();
157
+ applyMembers(entity, {
158
+ fields: opts.fields,
159
+ relations: Object.fromEntries(definedEntries(opts.relations ?? {}).map(([key, spec]) => [key, relationRegistration(spec)])),
160
+ hooks: Object.fromEntries(definedEntries(opts.hooks ?? {}).map(([event, methods]) => [event, methods(keys)])),
161
+ });
136
162
  // Unnamed checks are named by the generator, as unnamed indexes are.
137
163
  for (const check of opts.checks ?? []) {
138
164
  (meta.checks ??= []).push({ name: check.name, expression: ddlText(check.expression, 'a check constraint') });
@@ -267,12 +293,7 @@ export function removeEntity(entity) {
267
293
  return metas.delete(entity);
268
294
  }
269
295
  export function getEntities() {
270
- return metas.entries().reduce((acc, [key, val]) => {
271
- if (val.ids.length) {
272
- acc.push(key);
273
- }
274
- return acc;
275
- }, []);
296
+ return [...metas.values()].filter((meta) => meta.ids.length).map((meta) => meta.entity);
276
297
  }
277
298
  /**
278
299
  * The metadata of `entity`, marked as changed. Every `define*` goes through this, and nothing outside
@@ -308,11 +329,11 @@ export function getMeta(entity) {
308
329
  }
309
330
  function fillRelations(meta) {
310
331
  for (const [relKey, relation] of definedEntries(meta.relations)) {
311
- // The authored view: `mappedBy` may still be the callback and `references` unset until this settles them.
332
+ // The registered view: `references` may be unset until this settles it.
312
333
  const relOpts = relation;
313
334
  const at = `'${meta.entity.name}.${relKey}'`;
314
335
  if (relOpts.mappedBy) {
315
- fillInverseSide(at, meta, relOpts);
336
+ fillInverseSide(at, meta, relOpts, relOpts.mappedBy);
316
337
  }
317
338
  else if (!relOpts.references) {
318
339
  fillOwningSide(at, meta, relKey, relOpts);
@@ -326,8 +347,8 @@ function fillRelations(meta) {
326
347
  for (const { local } of relOpts.references) {
327
348
  if (junction.fields[local])
328
349
  continue;
329
- throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field. Declare it, or name ` +
330
- "the join columns with 'references'.");
350
+ throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field: a junction's ` +
351
+ 'columns are named after the entities it joins. Declare it.');
331
352
  }
332
353
  }
333
354
  }
@@ -337,9 +358,9 @@ function fillRelations(meta) {
337
358
  function fillOwningSide(at, meta, relKey, relOpts) {
338
359
  const relMeta = ensureMeta(relOpts.entity());
339
360
  if (relOpts.through) {
340
- // Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
341
- // `deleteRelations` and every dialect read them as junction columns. A composite key contributes
342
- // one pair per column of it, which is what makes the join address a whole key rather than part.
361
+ // Both columns live on the junction, whatever the cardinality: `deleteRelations` and every dialect
362
+ // read them as junction columns. A composite key contributes one pair per column of it, which is
363
+ // what makes the join address a whole key rather than part.
343
364
  relOpts.references = [
344
365
  ...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
345
366
  ...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
@@ -372,11 +393,9 @@ function fillOwningSide(at, meta, relKey, relOpts) {
372
393
  };
373
394
  }
374
395
  }
375
- function fillInverseSide(at, meta, relOpts) {
396
+ function fillInverseSide(at, meta, relOpts, mappedBy) {
376
397
  const relEntity = relOpts.entity();
377
398
  const relMeta = getMeta(relEntity);
378
- const mappedBy = getMappedByKey(relOpts);
379
- relOpts.mappedBy = mappedBy;
380
399
  if (relOpts.references)
381
400
  return;
382
401
  if (relMeta.fields[mappedBy]) {
@@ -448,13 +467,6 @@ function fillForeignKeyRelations(meta) {
448
467
  function junctionColumn(meta, idKey) {
449
468
  return lowerFirst(entityName(meta)) + upperFirst(fieldOf(meta, idKey).name ?? idKey);
450
469
  }
451
- /** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
452
- const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
453
- function getMappedByKey(relOpts) {
454
- return typeof relOpts.mappedBy === 'function'
455
- ? relOpts.mappedBy(RELATION_KEY_MAP)
456
- : relOpts.mappedBy;
457
- }
458
470
  /** Every key the entity marks, in declaration order. More than one is a composite primary key. */
459
471
  function getIdKeys(meta) {
460
472
  return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
@@ -49,8 +49,8 @@ export type HookContext<E extends object, Ctx = unknown> = {
49
49
  export type Hook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>) => void | Promise<void>;
50
50
  export type ResponseHook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>, envelope: RequestSuccessResponse<unknown>) => void | Promise<void>;
51
51
  export type RequestHandlerOptions<Ctx = unknown> = {
52
- include?: Type<any>[];
53
- exclude?: Type<any>[];
52
+ include?: Type<object>[];
53
+ exclude?: Type<object>[];
54
54
  /**
55
55
  * The URL segment an entity is addressed by, defaulting to its kebab-cased class name.
56
56
  *
@@ -27,7 +27,6 @@ export function createRequestHandler(opts) {
27
27
  "A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
28
28
  "pass an 'entityPath', or pass only one of them in 'include'.");
29
29
  }
30
- // oxlint-disable-next-line typescript/no-explicit-any -- heterogeneous entity map
31
30
  const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
32
31
  return (req) => {
33
32
  const entity = entityByPath.get(req.entityPath);
@@ -1,5 +1,6 @@
1
+ import { type RelationRows } from '../dialect/abstractSqlDialect.js';
1
2
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
2
- import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
+ import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
4
  export declare class MariaDialect extends MysqlLikeSqlDialect {
4
5
  readonly dialectName = "mariadb";
5
6
  readonly insertIdSource = "returning";
@@ -10,6 +11,11 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
10
11
  * and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
11
12
  */
12
13
  protected readonly featureOverrides: Partial<DialectFeatures>;
14
+ /**
15
+ * A derived table here reads no column of the statement around it, so the aggregate reads the
16
+ * related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
17
+ */
18
+ protected appendRelationArray(ctx: QueryContext, { entity, query, alias, joins }: RelationRows): void;
13
19
  protected upsertReturning<E>(meta: EntityMeta<E>): string;
14
20
  /**
15
21
  * MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
@@ -36,12 +42,13 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
36
42
  */
37
43
  protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
38
44
  /**
39
- * `SET STATEMENT mhnsw_ef_search=N FOR SELECT ...` - MariaDB scopes a variable to one statement, so
40
- * the tuning needs neither a transaction nor a restore afterwards, and cannot leak to the next
41
- * query on this pooled connection. That is why it prefixes the SQL here instead of coming back
42
- * from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
45
+ * `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
46
+ * neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
47
+ * which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
43
48
  */
44
- find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
49
+ protected statementSettings<E>(entity: Type<E>, q: Query<E>): string[];
50
+ /** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
51
+ protected applySettings(sql: string, settings: readonly string[]): string;
45
52
  /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
46
53
  protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
47
54
  }
@@ -1,3 +1,4 @@
1
+ import { relationTermKey } from '../dialect/abstractSqlDialect.js';
1
2
  import { jsonPath } from '../dialect/jsonSql.js';
2
3
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
4
  import { getMeta } from '../entity/index.js';
@@ -18,6 +19,24 @@ export class MariaDialect extends MysqlLikeSqlDialect {
18
19
  vectorIndexRequiresNotNull: true,
19
20
  indexIfNotExists: true,
20
21
  };
22
+ /**
23
+ * A derived table here reads no column of the statement around it, so the aggregate reads the
24
+ * related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
25
+ */
26
+ appendRelationArray(ctx, { entity, query, alias, joins }) {
27
+ const meta = getMeta(entity);
28
+ const terms = this.projection(ctx, entity, query, { prefix: alias, json: true }, joins);
29
+ const sortOpts = { prefix: alias, joins, distinct: query.$distinct };
30
+ const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query.$sort, sortOpts));
31
+ const page = this.buildFragment(ctx, (fragmentCtx) => this.pager(fragmentCtx, query));
32
+ const from = this.buildFragment(ctx, (fragmentCtx) => {
33
+ this.selectRelationJoins(fragmentCtx, meta, alias, joins);
34
+ this.where(fragmentCtx, entity, query.$where, { prefix: alias });
35
+ });
36
+ const object = this.jsonObject(terms.map((term) => [relationTermKey(term), term.sql]));
37
+ const rows = `${query.$distinct ? 'DISTINCT ' : ''}${object}${order}${page}`;
38
+ ctx.append(`COALESCE((SELECT JSON_ARRAYAGG(${rows}) FROM ${this.tableRef(meta, alias).ref}${from}), JSON_ARRAY())`);
39
+ }
21
40
  upsertReturning(meta) {
22
41
  const returning = this.returningId(meta);
23
42
  return returning ? ` ${returning}` : '';
@@ -61,18 +80,19 @@ export class MariaDialect extends MysqlLikeSqlDialect {
61
80
  ctx.append(')');
62
81
  }
63
82
  /**
64
- * `SET STATEMENT mhnsw_ef_search=N FOR SELECT ...` - MariaDB scopes a variable to one statement, so
65
- * the tuning needs neither a transaction nor a restore afterwards, and cannot leak to the next
66
- * query on this pooled connection. That is why it prefixes the SQL here instead of coming back
67
- * from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
83
+ * `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
84
+ * neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
85
+ * which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
68
86
  */
69
- find(ctx, entity, q = {}, opts, totalAlias) {
87
+ statementSettings(entity, q) {
70
88
  // `$candidates` first: `getMeta` would otherwise be resolved on every read, to discover that
71
89
  // almost none of them tune anything.
72
- if (q.$candidates !== undefined && this.tunedVectorIndex(getMeta(entity), q)) {
73
- ctx.append(`SET STATEMENT mhnsw_ef_search=${q.$candidates} FOR `);
74
- }
75
- super.find(ctx, entity, q, opts, totalAlias);
90
+ const tuned = q.$candidates !== undefined && this.tunedVectorIndex(getMeta(entity), q);
91
+ return [...(tuned ? [`mhnsw_ef_search=${q.$candidates}`] : []), ...super.statementSettings(entity, q)];
92
+ }
93
+ /** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
94
+ applySettings(sql, settings) {
95
+ return `SET STATEMENT ${settings.join(', ')} FOR ${sql}`;
76
96
  }
77
97
  /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
78
98
  selectFieldExpr(escapedColumn, field) {