uql-orm 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +4 -4
  3. package/dist/dialect/abstractSqlDialect.d.ts +12 -7
  4. package/dist/dialect/abstractSqlDialect.js +51 -50
  5. package/dist/dialect/mysqlLikeSqlDialect.js +1 -1
  6. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  7. package/dist/dialect/pgLikeSqlDialect.js +8 -7
  8. package/dist/dialect/queryContext.d.ts +5 -6
  9. package/dist/dialect/queryContext.js +8 -8
  10. package/dist/entity/decorator/entity.d.ts +3 -3
  11. package/dist/entity/decorator/entity.js +2 -2
  12. package/dist/entity/decorator/members.d.ts +3 -2
  13. package/dist/entity/decorator/members.js +1 -0
  14. package/dist/entity/metadata/definition.js +12 -6
  15. package/dist/http/contract.d.ts +1 -1
  16. package/dist/http/contract.js +16 -4
  17. package/dist/migrate/builder/migrationBuilder.js +3 -5
  18. package/dist/migrate/builder/tableBuilder.js +2 -4
  19. package/dist/migrate/builder/types.d.ts +11 -3
  20. package/dist/migrate/codegen/indexDecoratorSource.js +3 -1
  21. package/dist/migrate/generator/definitionToNode.d.ts +8 -5
  22. package/dist/migrate/generator/definitionToNode.js +13 -4
  23. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  24. package/dist/migrate/generator/mongoSchemaGenerator.js +6 -1
  25. package/dist/migrate/schemaGenerator.d.ts +5 -3
  26. package/dist/migrate/schemaGenerator.js +9 -2
  27. package/dist/mongo/mongoDialect.js +1 -1
  28. package/dist/mssql/mssqlDialect.js +3 -5
  29. package/dist/querier/queryError.d.ts +15 -12
  30. package/dist/querier/queryError.js +67 -8
  31. package/dist/schema/schemaASTBuilder.d.ts +6 -1
  32. package/dist/schema/schemaASTBuilder.js +17 -16
  33. package/dist/type/dialect.d.ts +15 -6
  34. package/dist/type/entity.d.ts +94 -34
  35. package/dist/type/migration.d.ts +9 -2
  36. package/dist/type/query.d.ts +3 -3
  37. package/dist/type/queryLock.d.ts +7 -7
  38. package/dist/type/queryLock.js +8 -6
  39. package/dist/type/queryRaw.d.ts +22 -30
  40. package/dist/type/queryRaw.js +3 -9
  41. package/dist/util/ddlExpression.util.d.ts +8 -13
  42. package/dist/util/ddlExpression.util.js +17 -19
  43. package/dist/util/dialect.util.d.ts +1 -1
  44. package/dist/util/dialect.util.js +3 -4
  45. package/dist/util/field.util.d.ts +1 -1
  46. package/dist/util/raw.d.ts +19 -17
  47. package/dist/util/raw.js +43 -17
  48. package/package.json +1 -1
@@ -1,43 +1,36 @@
1
1
  import type { QueryContext, QueryDialect } from './dialect.js';
2
- import type { Scalar } from './utility.js';
3
- /**
4
- * What may be passed towards a `raw` callback. Every key is optional here because the callers along the
5
- * way fill them in progressively; what reaches the callback is the complete set - see {@link QueryRawFn}.
6
- */
7
- export type QueryRawFnOptions = {
8
- /**
9
- * the current dialect.
10
- */
11
- dialect?: QueryDialect;
12
- /**
13
- * the prefix.
14
- */
15
- prefix?: string;
16
- /**
17
- * the escaped prefix.
18
- */
19
- escapedPrefix?: string;
2
+ import type { Type } from './utility.js';
3
+ /** What a `raw` callback receives. See {@link QueryRawFn}. */
4
+ export type QueryRawRenderOptions = {
5
+ /** The dialect rendering the SQL. */
6
+ dialect: QueryDialect;
7
+ /** The alias of the table in scope, unescaped; empty where there is none. */
8
+ prefix: string;
9
+ /** {@link prefix} escaped, with its trailing dot. */
10
+ escapedPrefix: string;
11
+ /** The query context the SQL is written into. */
12
+ ctx: QueryContext;
20
13
  /**
21
- * the query context.
14
+ * The entity being rendered, which a ref read off a definition's map resolves its column against: a
15
+ * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.
22
16
  */
23
- ctx?: QueryContext;
17
+ entity?: Type<unknown>;
24
18
  };
19
+ /** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */
20
+ export type QueryRawFnOptions = Partial<QueryRawRenderOptions>;
25
21
  /**
26
22
  * A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else
27
23
  * it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the
28
24
  * latter rejected `({ ctx }) => ctx.append(...)`, the form every computed field is written in, because
29
25
  * TypeScript's "returning a value where void is expected" allowance does not apply to a union.
30
- *
31
- * `Required`, and the parameter not optional, because the one place that calls it (`getRawValue`)
32
- * passes all four every time.
33
26
  */
34
- export type QueryRawFn = (opts: Required<QueryRawFnOptions>) => unknown;
27
+ export type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;
35
28
  export declare const RAW_VALUE: unique symbol;
36
29
  export declare const RAW_ALIAS: unique symbol;
37
30
  export declare class QueryRaw {
38
- readonly [RAW_VALUE]: Scalar | QueryRawFn;
31
+ readonly [RAW_VALUE]: QueryRawFn;
39
32
  readonly [RAW_ALIAS]?: string;
40
- constructor(value: Scalar | QueryRawFn, alias?: string);
33
+ constructor(value: QueryRawFn, alias?: string);
41
34
  /** The same expression under an alias, for a `$select` projection. */
42
35
  as(alias: string): QueryRaw;
43
36
  /**
@@ -45,9 +38,8 @@ export declare class QueryRaw {
45
38
  * business, which is what lets a `raw` tagged template resolve an interpolated fragment without
46
39
  * the dialect having to expose a method for it.
47
40
  *
48
- * The alias is not emitted here: it belongs to a `$select` projection, not to an expression, and
49
- * a fragment nested inside another would otherwise emit one mid-expression. `getRawValue` appends
50
- * it around this call.
41
+ * The alias is not emitted here: it names a `$select` projection, which writes it after the term,
42
+ * and anywhere else it would land mid-expression.
51
43
  */
52
- render(opts: Required<QueryRawFnOptions>): void;
44
+ render(opts: QueryRawRenderOptions): void;
53
45
  }
@@ -16,17 +16,11 @@ export class QueryRaw {
16
16
  * business, which is what lets a `raw` tagged template resolve an interpolated fragment without
17
17
  * the dialect having to expose a method for it.
18
18
  *
19
- * The alias is not emitted here: it belongs to a `$select` projection, not to an expression, and
20
- * a fragment nested inside another would otherwise emit one mid-expression. `getRawValue` appends
21
- * it around this call.
19
+ * The alias is not emitted here: it names a `$select` projection, which writes it after the term,
20
+ * and anywhere else it would land mid-expression.
22
21
  */
23
22
  render(opts) {
24
- const value = this[RAW_VALUE];
25
- if (typeof value !== 'function') {
26
- opts.ctx.append(opts.prefix + String(value));
27
- return;
28
- }
29
- const emitted = value(opts);
23
+ const emitted = this[RAW_VALUE](opts);
30
24
  if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {
31
25
  opts.ctx.append(String(emitted));
32
26
  }
@@ -1,15 +1,10 @@
1
- import { type IndexColumnInput, type IndexColumnSchema, QueryRaw } from '../type/index.js';
1
+ import { type EntityIndexColumn, type IndexColumnInput, type IndexColumnSchema, QueryRaw } from '../type/index.js';
2
2
  /**
3
- * SQL bound for DDL, as the text a generator renders. `raw` with no interpolation, or a bare string
4
- * where one is still accepted: DDL is evaluated once at creation time, so there is no query context
5
- * for the callback form and no placeholder a `CREATE` statement could bind a value into.
6
- *
7
- * `what` names the thing being declared, so the error says which one the caller got wrong.
3
+ * Reduces an authored index entry to the form metadata keeps, so the shapes users write - a column
4
+ * name, an expression's callback, an options object - reach the schema as one, each callback resolved.
8
5
  */
9
- export declare function ddlText(value: string | QueryRaw, what: string): string;
10
- export declare function ddlText(value: string | QueryRaw | undefined, what: string): string | undefined;
11
- /**
12
- * Reduces an authored index entry to its normalized form, so the three shapes users write - a column
13
- * name, an expression, or an options object - reach the dialects as one.
14
- */
15
- export declare function normalizeIndexColumn(entry: IndexColumnInput): IndexColumnSchema;
6
+ export declare function normalizeIndexColumn(entry: IndexColumnInput): EntityIndexColumn;
7
+ /** An index entry as the schema holds it, its expression rendered to text by `render`. */
8
+ export declare function renderIndexColumn(entry: EntityIndexColumn, render: (sql: QueryRaw) => string): IndexColumnSchema;
9
+ /** What an unnamed index's name is built from: each entry's column, or `expr<n>` for an expression, which has none. */
10
+ export declare function indexNameParts(entries: readonly EntityIndexColumn[]): string[];
@@ -1,27 +1,25 @@
1
- import { QueryRaw, RAW_VALUE } from '../type/index.js';
2
- export function ddlText(value, what) {
3
- if (!(value instanceof QueryRaw)) {
4
- return value;
5
- }
6
- const sql = value[RAW_VALUE];
7
- if (typeof sql !== 'string') {
8
- throw new TypeError(`${what} needs raw() with no interpolation, not a function or a bound value`);
9
- }
10
- return sql;
11
- }
1
+ import { QueryRaw } from '../type/index.js';
2
+ import { entitySql } from './raw.js';
12
3
  /**
13
- * Reduces an authored index entry to its normalized form, so the three shapes users write - a column
14
- * name, an expression, or an options object - reach the dialects as one.
4
+ * Reduces an authored index entry to the form metadata keeps, so the shapes users write - a column
5
+ * name, an expression's callback, an options object - reach the schema as one, each callback resolved.
15
6
  */
16
7
  export function normalizeIndexColumn(entry) {
17
8
  if (typeof entry === 'string') {
18
9
  return { column: entry };
19
10
  }
20
- if (entry instanceof QueryRaw) {
21
- return { column: ddlText(entry, 'an index expression'), expression: true };
11
+ if (typeof entry === 'function') {
12
+ return { column: entitySql(entry) };
22
13
  }
23
- const { column, ...rest } = entry;
24
- return column instanceof QueryRaw
25
- ? { ...rest, column: ddlText(column, 'an index expression'), expression: true }
26
- : { ...rest, column };
14
+ const { column } = entry;
15
+ return { ...entry, column: typeof column === 'function' ? entitySql(column) : column };
16
+ }
17
+ /** An index entry as the schema holds it, its expression rendered to text by `render`. */
18
+ export function renderIndexColumn(entry, render) {
19
+ const { column } = entry;
20
+ return column instanceof QueryRaw ? { ...entry, column: render(column), expression: true } : { ...entry, column };
21
+ }
22
+ /** What an unnamed index's name is built from: each entry's column, or `expr<n>` for an expression, which has none. */
23
+ export function indexNameParts(entries) {
24
+ return entries.map((entry, at) => (typeof entry.column === 'string' ? entry.column : `expr${at}`));
27
25
  }
@@ -83,7 +83,7 @@ export declare function findVectorSort<E>(sort: QuerySortMap<E> | undefined): {
83
83
  * The vector index declared on `key`, if any. Answers both "is there an ANN index to tune here" and
84
84
  * "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
85
85
  */
86
- export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): EntityIndexMeta | undefined;
86
+ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): EntityIndexMeta<E> | undefined;
87
87
  /**
88
88
  * Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
89
89
  * What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
@@ -222,7 +222,7 @@ export function hasVectorNear(where) {
222
222
  return Object.entries(where).some(([key, value]) => key === '$near' || hasVectorNear(value));
223
223
  }
224
224
  function indexCoversColumn(index, key) {
225
- return index.columns.some((entry) => !(entry instanceof QueryRaw) && entry.column === key);
225
+ return index.columns.some((entry) => entry.column === key);
226
226
  }
227
227
  /** `satisfies` ties this to {@link JsonUpdateOp}, so renaming an operator breaks it at compile time. */
228
228
  const JSON_UPDATE_OPS = [
@@ -293,8 +293,7 @@ export function applyFilters(meta, whereMap, opts) {
293
293
  if (!active) {
294
294
  continue;
295
295
  }
296
- const raw = filter.condition;
297
- const condition = typeof raw === 'function' ? raw(context) : raw;
296
+ const condition = typeof filter.where === 'function' ? filter.where(context) : filter.where;
298
297
  if (condition === undefined) {
299
298
  const onMissing = filter.onMissing ?? (filter.security ? 'throw' : 'skip');
300
299
  if (onMissing === 'throw') {
@@ -444,7 +443,7 @@ export function textSearchFields(meta, search) {
444
443
  }
445
444
  const fulltext = (meta.indexes ?? []).filter((index) => index.type === 'fulltext');
446
445
  if (fulltext.length === 1) {
447
- return fulltext[0].columns.map((entry) => entry.column);
446
+ return fulltext[0].columns.flatMap((entry) => (typeof entry.column === 'string' ? [entry.column] : []));
448
447
  }
449
448
  const name = entityName(meta);
450
449
  const declared = fulltext.length
@@ -32,7 +32,7 @@ export declare function isIntegerColumn(field: Pick<FieldOptions, 'type' | 'colu
32
32
  * Every read site asks this - the DDL skip, the projection, the `$where` and `ORDER BY` operands -
33
33
  * because an inlined field has no column to name, while a stored one is read like any other.
34
34
  */
35
- export declare function isInlinedExpression(field: FieldOptions): boolean;
35
+ export declare function isInlinedExpression<F extends FieldOptions>(field: F): field is F & Required<Pick<F, 'computed'>>;
36
36
  /**
37
37
  * Whether the database supplies this field's value, so no insert or update may write it: a stored
38
38
  * computed column *is* a real column, read like one, but writing to it is an error on every engine.
@@ -1,36 +1,38 @@
1
- import { QueryRaw, type QueryRawFn } from '../type/index.js';
1
+ import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type RefMap, type Type } from '../type/index.js';
2
2
  /**
3
3
  * Create a raw SQL expression.
4
4
  *
5
5
  * As a tagged template the literal text is emitted as written and every interpolation is resolved by
6
6
  * what it is, so a value cannot become SQL whatever it holds:
7
7
  *
8
- * | Interpolated | Becomes |
9
- * | :----------------- | :---------------------- |
10
- * | any value | a bound parameter |
11
- * | a {@link QueryRaw} | that fragment, in place |
8
+ * | Interpolated | Becomes |
9
+ * | :------------------ | :------------------------------------- |
10
+ * | any value | a bound parameter; in DDL, its literal |
11
+ * | a {@link ColumnRef} | its column, escaped and qualified |
12
+ * | a {@link QueryRaw} | that fragment, in place |
12
13
  *
13
14
  * ```ts
14
- * raw`GREATEST(0, "creditsAllowance" - ${amount})`
15
- * raw`CONCAT(${col('firstName')}, ' ', ${col('lastName')})`
15
+ * const user = refs(User);
16
+ * raw`GREATEST(0, ${user.creditsAllowance} - ${amount})`
17
+ * raw`CONCAT(${user.firstName}, ' ', ${user.lastName})`
16
18
  * raw`LOG10(${points})`.as('score')
17
19
  * ```
18
20
  *
19
21
  * The callback form remains for SQL a template cannot express, such as a sub-query generated through
20
- * `dialect.find(...)`. See {@link col} for a context-aware column reference.
22
+ * `dialect.find(...)`.
21
23
  *
22
24
  * **⚠️ Security:** the tag is safe because it binds; a callback is not, since it emits whatever it
23
25
  * writes, so never build one from user input. Inside a callback, bind with `ctx.addValue()`.
24
26
  */
25
27
  export declare function raw(strings: TemplateStringsArray, ...values: readonly unknown[]): QueryRaw;
26
- export declare function raw(value: QueryRawFn, alias?: string): QueryRaw;
28
+ export declare function raw(value: QueryRawFn): QueryRaw;
27
29
  /**
28
- * A column of the entity being queried, alias-qualified and escaped for the dialect. This is what a
29
- * template cannot know on its own: the alias is decided while the statement is built, not where the
30
- * expression is written.
31
- *
32
- * Takes the column name as it exists in the database, not the entity's field name: no entity metadata
33
- * is in scope here, so a naming strategy is not applied for you. `escapedPrefix` already carries its
34
- * trailing dot, which is the detail this exists to stop you getting wrong.
30
+ * The fields of `entity` as {@link ColumnRef}s, each rendering inside `raw` as its column: named the way
31
+ * the dialect names it, so the naming strategy and `@Field({ name })` apply, and qualified by the alias
32
+ * in scope. Metadata is read when a ref renders, so the map serves before the fields are registered.
35
33
  */
36
- export declare function col(column: string): QueryRaw;
34
+ export declare function refs<E>(entity: Type<E>): RefMap<E>;
35
+ /** SQL a definition writes, a callback's refs read off {@link MEMBER_REFS}. */
36
+ export declare function entitySql<E>(sql: EntitySql<E>): QueryRaw;
37
+ /** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
38
+ export declare function entityWhere<E>(where: EntityWhere<E>): EntityWhereMeta<E>;
package/dist/util/raw.js CHANGED
@@ -1,13 +1,9 @@
1
- import { QueryRaw } from '../type/index.js';
1
+ import { getMeta } from '../entity/metadata/definition.js';
2
+ import { QueryRaw, } from '../type/index.js';
3
+ import { isInlinedExpression } from './field.util.js';
2
4
  export function raw(value, ...rest) {
3
- const [alias] = rest;
4
5
  if (!isTemplateStrings(value)) {
5
- return new QueryRaw(value, typeof alias === 'string' ? alias : undefined);
6
- }
7
- if (!rest.length) {
8
- // Nothing to bind, so this is the string form: keep it one, for the DDL paths that need to read
9
- // the expression back as text (an index expression cannot carry a parameter).
10
- return new QueryRaw(value[0]);
6
+ return new QueryRaw(value);
11
7
  }
12
8
  return new QueryRaw((opts) => {
13
9
  const { ctx } = opts;
@@ -24,16 +20,46 @@ export function raw(value, ...rest) {
24
20
  });
25
21
  }
26
22
  /**
27
- * A column of the entity being queried, alias-qualified and escaped for the dialect. This is what a
28
- * template cannot know on its own: the alias is decided while the statement is built, not where the
29
- * expression is written.
30
- *
31
- * Takes the column name as it exists in the database, not the entity's field name: no entity metadata
32
- * is in scope here, so a naming strategy is not applied for you. `escapedPrefix` already carries its
33
- * trailing dot, which is the detail this exists to stop you getting wrong.
23
+ * The fields of `entity` as {@link ColumnRef}s, each rendering inside `raw` as its column: named the way
24
+ * the dialect names it, so the naming strategy and `@Field({ name })` apply, and qualified by the alias
25
+ * in scope. Metadata is read when a ref renders, so the map serves before the fields are registered.
34
26
  */
35
- export function col(column) {
36
- return new QueryRaw(({ escapedPrefix, dialect }) => escapedPrefix + dialect.escapeId(column, true));
27
+ export function refs(entity) {
28
+ return new Proxy({}, { get: (_, key) => columnRef(entity, String(key)) });
29
+ }
30
+ /**
31
+ * The refs a definition's callback reads. A member decorator sees no class, so these name no entity and
32
+ * resolve against the one rendering them: a computed field's own, or the one whose schema is built.
33
+ */
34
+ const MEMBER_REFS = new Proxy({}, { get: (_, key) => columnRef(undefined, String(key)) });
35
+ /** SQL a definition writes, a callback's refs read off {@link MEMBER_REFS}. */
36
+ export function entitySql(sql) {
37
+ return sql instanceof QueryRaw ? sql : sql(MEMBER_REFS);
38
+ }
39
+ /** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
40
+ export function entityWhere(where) {
41
+ return typeof where === 'function' ? where(MEMBER_REFS) : where;
42
+ }
43
+ /** One field as SQL, against its own entity or, read off a definition, the entity rendering it. */
44
+ function columnRef(entity, key) {
45
+ return new QueryRaw((opts) => {
46
+ const owner = entity ?? opts.entity;
47
+ if (!owner) {
48
+ throw new TypeError(`'${key}' was read off a definition's refs, so it renders only inside its entity's SQL`);
49
+ }
50
+ renderColumn(getMeta(owner), key, { ...opts, entity: owner });
51
+ });
52
+ }
53
+ /** A field's column, or the expression an inlined computed one stands for, as a `$where` on it reads it. */
54
+ function renderColumn(meta, key, opts) {
55
+ const field = meta.fields[key];
56
+ if (field && isInlinedExpression(field)) {
57
+ opts.ctx.append('(');
58
+ field.computed.render(opts);
59
+ opts.ctx.append(')');
60
+ return;
61
+ }
62
+ opts.ctx.append(opts.escapedPrefix + opts.dialect.escapeId(opts.dialect.columnOf(meta, key), true));
37
63
  }
38
64
  /** A tag call passes the frozen strings array, which carries its own `raw` counterpart. */
39
65
  function isTemplateStrings(value) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.60.0",
6
+ "version": "0.62.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"