metal-orm 1.0.96 → 1.0.98

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.
@@ -1,125 +1,125 @@
1
- import { TableDef } from '../../schema/table.js';
2
- import { SelectQueryBuilder } from '../../query-builder/select.js';
3
- import { ExpressionNode, inList, LiteralNode } from '../../core/ast/expression.js';
4
- import type { QueryResult } from '../../core/execution/db-executor.js';
5
- import { ColumnDef } from '../../schema/column-types.js';
6
- import { EntityContext } from '../entity-context.js';
7
-
8
- /**
9
- * An array of database rows, each represented as a record of string keys to unknown values.
10
- */
11
- export type Rows = Record<string, unknown>[];
12
-
13
- /**
14
- * Represents a single tracked entity from the EntityContext for a table.
15
- */
16
- export type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
17
-
18
- export const hasColumns = (columns?: readonly string[]): columns is readonly string[] =>
19
- Boolean(columns && columns.length > 0);
20
-
21
- export const buildColumnSelection = (
22
- table: TableDef,
23
- columns: string[],
24
- missingMsg: (col: string) => string
25
- ): Record<string, ColumnDef> => {
26
- return columns.reduce((acc, column) => {
27
- const def = table.columns[column];
28
- if (!def) {
29
- throw new Error(missingMsg(column));
30
- }
31
- acc[column] = def;
32
- return acc;
33
- }, {} as Record<string, ColumnDef>);
34
- };
35
-
36
- export const filterRow = (row: Record<string, unknown>, columns: Set<string>): Record<string, unknown> => {
37
- const filtered: Record<string, unknown> = {};
38
- for (const column of columns) {
39
- if (column in row) {
40
- filtered[column] = row[column];
41
- }
42
- }
43
- return filtered;
44
- };
45
-
46
- export const filterRows = (rows: Rows, columns: Set<string>): Rows => rows.map(row => filterRow(row, columns));
47
-
48
- const rowsFromResults = (results: QueryResult[]): Rows => {
49
- const rows: Rows = [];
50
- for (const result of results) {
51
- const { columns, values } = result;
52
- for (const valueRow of values) {
53
- const row: Record<string, unknown> = {};
54
- columns.forEach((column, idx) => {
55
- row[column] = valueRow[idx];
56
- });
57
- rows.push(row);
58
- }
59
- }
60
- return rows;
61
- };
62
-
63
- const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
64
- const compiled = ctx.dialect.compileSelect(qb.getAST());
65
- const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
66
- return rowsFromResults(results);
67
- };
68
-
69
- export const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
70
-
71
- export const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
72
- const collected = new Set<unknown>();
73
- for (const tracked of roots) {
74
- const value = (tracked.entity as Record<string, unknown>)[key];
75
- if (value !== null && value !== undefined) {
76
- collected.add(value);
77
- }
78
- }
79
- return collected;
80
- };
81
-
82
- const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
83
- Array.from(keys) as (string | number | LiteralNode)[];
84
-
85
- export const fetchRowsForKeys = async (
86
- ctx: EntityContext,
87
- table: TableDef,
88
- column: ColumnDef,
89
- keys: Set<unknown>,
90
- selection: Record<string, ColumnDef>,
91
- filter?: ExpressionNode
92
- ): Promise<Rows> => {
93
- let qb = new SelectQueryBuilder(table).select(selection);
94
- qb = qb.where(inList(column, buildInListValues(keys)));
95
- if (filter) {
96
- qb = qb.where(filter);
97
- }
98
- return executeQuery(ctx, qb);
99
- };
100
-
101
- export const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
102
- const grouped = new Map<string, Rows>();
103
- for (const row of rows) {
104
- const value = row[keyColumn];
105
- if (value === null || value === undefined) continue;
106
- const key = toKey(value);
107
- const bucket = grouped.get(key) ?? [];
108
- bucket.push(row);
109
- grouped.set(key, bucket);
110
- }
111
- return grouped;
112
- };
113
-
114
- export const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
115
- const lookup = new Map<string, Record<string, unknown>>();
116
- for (const row of rows) {
117
- const value = row[keyColumn];
118
- if (value === null || value === undefined) continue;
119
- const key = toKey(value);
120
- if (!lookup.has(key)) {
121
- lookup.set(key, row);
122
- }
123
- }
124
- return lookup;
125
- };
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { SelectQueryBuilder } from '../../query-builder/select.js';
3
+ import { ExpressionNode, inList, LiteralNode } from '../../core/ast/expression.js';
4
+ import type { QueryResult } from '../../core/execution/db-executor.js';
5
+ import { ColumnDef } from '../../schema/column-types.js';
6
+ import { EntityContext } from '../entity-context.js';
7
+
8
+ /**
9
+ * An array of database rows, each represented as a record of string keys to unknown values.
10
+ */
11
+ export type Rows = Record<string, unknown>[];
12
+
13
+ /**
14
+ * Represents a single tracked entity from the EntityContext for a table.
15
+ */
16
+ export type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
17
+
18
+ export const hasColumns = (columns?: readonly string[]): columns is readonly string[] =>
19
+ Boolean(columns && columns.length > 0);
20
+
21
+ export const buildColumnSelection = (
22
+ table: TableDef,
23
+ columns: string[],
24
+ missingMsg: (col: string) => string
25
+ ): Record<string, ColumnDef> => {
26
+ return columns.reduce((acc, column) => {
27
+ const def = table.columns[column];
28
+ if (!def) {
29
+ throw new Error(missingMsg(column));
30
+ }
31
+ acc[column] = def;
32
+ return acc;
33
+ }, {} as Record<string, ColumnDef>);
34
+ };
35
+
36
+ export const filterRow = (row: Record<string, unknown>, columns: Set<string>): Record<string, unknown> => {
37
+ const filtered: Record<string, unknown> = {};
38
+ for (const column of columns) {
39
+ if (column in row) {
40
+ filtered[column] = row[column];
41
+ }
42
+ }
43
+ return filtered;
44
+ };
45
+
46
+ export const filterRows = (rows: Rows, columns: Set<string>): Rows => rows.map(row => filterRow(row, columns));
47
+
48
+ const rowsFromResults = (results: QueryResult[]): Rows => {
49
+ const rows: Rows = [];
50
+ for (const result of results) {
51
+ const { columns, values } = result;
52
+ for (const valueRow of values) {
53
+ const row: Record<string, unknown> = {};
54
+ columns.forEach((column, idx) => {
55
+ row[column] = valueRow[idx];
56
+ });
57
+ rows.push(row);
58
+ }
59
+ }
60
+ return rows;
61
+ };
62
+
63
+ const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
64
+ const compiled = ctx.dialect.compileSelect(qb.getAST());
65
+ const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
66
+ return rowsFromResults(results);
67
+ };
68
+
69
+ export const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
70
+
71
+ export const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
72
+ const collected = new Set<unknown>();
73
+ for (const tracked of roots) {
74
+ const value = (tracked.entity as Record<string, unknown>)[key];
75
+ if (value !== null && value !== undefined) {
76
+ collected.add(value);
77
+ }
78
+ }
79
+ return collected;
80
+ };
81
+
82
+ const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
83
+ Array.from(keys) as (string | number | LiteralNode)[];
84
+
85
+ export const fetchRowsForKeys = async (
86
+ ctx: EntityContext,
87
+ table: TableDef,
88
+ column: ColumnDef,
89
+ keys: Set<unknown>,
90
+ selection: Record<string, ColumnDef>,
91
+ filter?: ExpressionNode
92
+ ): Promise<Rows> => {
93
+ let qb = new SelectQueryBuilder(table).select(selection);
94
+ qb = qb.where(inList(column, buildInListValues(keys)));
95
+ if (filter) {
96
+ qb = qb.where(filter);
97
+ }
98
+ return executeQuery(ctx, qb);
99
+ };
100
+
101
+ export const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
102
+ const grouped = new Map<string, Rows>();
103
+ for (const row of rows) {
104
+ const value = row[keyColumn];
105
+ if (value === null || value === undefined) continue;
106
+ const key = toKey(value);
107
+ const bucket = grouped.get(key) ?? [];
108
+ bucket.push(row);
109
+ grouped.set(key, bucket);
110
+ }
111
+ return grouped;
112
+ };
113
+
114
+ export const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
115
+ const lookup = new Map<string, Record<string, unknown>>();
116
+ for (const row of rows) {
117
+ const value = row[keyColumn];
118
+ if (value === null || value === undefined) continue;
119
+ const key = toKey(value);
120
+ if (!lookup.has(key)) {
121
+ lookup.set(key, row);
122
+ }
123
+ }
124
+ return lookup;
125
+ };
@@ -28,14 +28,14 @@ import type { PrimaryKey } from './entity-context.js';
28
28
  export interface SaveGraphOptions {
29
29
  /** Remove existing collection members that are not present in the payload */
30
30
  pruneMissing?: boolean;
31
- /**
32
- * Coerce JSON-friendly input values into DB-friendly primitives.
33
- * Currently:
34
- * - `json`: Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
35
- * - `json-in`: string/number -> Date (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
36
- */
37
- coerce?: 'json' | 'json-in';
38
- }
31
+ /**
32
+ * Coerce JSON-friendly input values into DB-friendly primitives.
33
+ * Currently:
34
+ * - `json`: Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
35
+ * - `json-in`: string/number -> Date (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
36
+ */
37
+ coerce?: 'json' | 'json-in';
38
+ }
39
39
 
40
40
  /** Represents an entity object with arbitrary properties. */
41
41
  type AnyEntity = Record<string, unknown>;
@@ -52,49 +52,49 @@ type AnyEntity = Record<string, unknown>;
52
52
 
53
53
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
54
54
 
55
- const coerceColumnValue = (
56
- table: TableDef,
57
- columnName: string,
58
- value: unknown,
59
- options: SaveGraphOptions
60
- ): unknown => {
61
- if (value === null || value === undefined) return value;
62
-
63
- const column = table.columns[columnName] as unknown as ColumnDef | undefined;
64
- if (!column) return value;
55
+ const coerceColumnValue = (
56
+ table: TableDef,
57
+ columnName: string,
58
+ value: unknown,
59
+ options: SaveGraphOptions
60
+ ): unknown => {
61
+ if (value === null || value === undefined) return value;
62
+
63
+ const column = table.columns[columnName] as unknown as ColumnDef | undefined;
64
+ if (!column) return value;
65
65
 
66
66
  const normalized = normalizeColumnType(column.type);
67
67
 
68
- const isDateLikeColumn =
69
- normalized === 'date' ||
70
- normalized === 'datetime' ||
71
- normalized === 'timestamp' ||
72
- normalized === 'timestamptz';
73
-
74
- if (!isDateLikeColumn) return value;
75
-
76
- if (options.coerce === 'json') {
77
- if (value instanceof Date) {
78
- return value.toISOString();
79
- }
80
- return value;
81
- }
82
-
83
- if (options.coerce === 'json-in') {
84
- if (value instanceof Date) return value;
85
- if (typeof value === 'string' || typeof value === 'number') {
86
- const date = new Date(value);
87
- if (Number.isNaN(date.getTime())) {
88
- throw new Error(`Invalid date value for column "${columnName}"`);
89
- }
90
- return date;
91
- }
92
- return value;
93
- }
94
-
95
- // Future coercions can be added here based on `normalized`.
96
- return value;
97
- };
68
+ const isDateLikeColumn =
69
+ normalized === 'date' ||
70
+ normalized === 'datetime' ||
71
+ normalized === 'timestamp' ||
72
+ normalized === 'timestamptz';
73
+
74
+ if (!isDateLikeColumn) return value;
75
+
76
+ if (options.coerce === 'json') {
77
+ if (value instanceof Date) {
78
+ return value.toISOString();
79
+ }
80
+ return value;
81
+ }
82
+
83
+ if (options.coerce === 'json-in') {
84
+ if (value instanceof Date) return value;
85
+ if (typeof value === 'string' || typeof value === 'number') {
86
+ const date = new Date(value);
87
+ if (Number.isNaN(date.getTime())) {
88
+ throw new Error(`Invalid date value for column "${columnName}"`);
89
+ }
90
+ return date;
91
+ }
92
+ return value;
93
+ }
94
+
95
+ // Future coercions can be added here based on `normalized`.
96
+ return value;
97
+ };
98
98
 
99
99
  const pickColumns = (table: TableDef, payload: AnyEntity, options: SaveGraphOptions): Record<string, unknown> => {
100
100
  const columns: Record<string, unknown> = {};
@@ -69,13 +69,13 @@ export class ColumnSelector {
69
69
  * @param columns - Columns to make distinct
70
70
  * @returns Updated query context with DISTINCT clause
71
71
  */
72
- distinct(context: SelectQueryBuilderContext, columns: (ColumnDef | ColumnNode)[]): SelectQueryBuilderContext {
73
- const from = context.state.ast.from;
74
- const tableRef = from.type === 'Table' && from.alias ? { ...this.env.table, alias: from.alias } : this.env.table;
75
- const nodes = columns.map(col => buildColumnNode(tableRef, col));
76
- const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
77
- const nextState = astService.withDistinct(nodes);
78
- return { state: nextState, hydration: context.hydration };
79
- }
80
- }
72
+ distinct(context: SelectQueryBuilderContext, columns: (ColumnDef | ColumnNode)[]): SelectQueryBuilderContext {
73
+ const from = context.state.ast.from;
74
+ const tableRef = from.type === 'Table' && from.alias ? { ...this.env.table, alias: from.alias } : this.env.table;
75
+ const nodes = columns.map(col => buildColumnNode(tableRef, col));
76
+ const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
77
+ const nextState = astService.withDistinct(nodes);
78
+ return { state: nextState, hydration: context.hydration };
79
+ }
80
+ }
81
81