metal-orm 1.0.1 → 1.0.3

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/.github/workflows/publish-metal-orm.yml +38 -0
  2. package/README.md +46 -482
  3. package/docs/advanced-features.md +85 -0
  4. package/docs/api-reference.md +22 -0
  5. package/docs/getting-started.md +104 -0
  6. package/docs/hydration.md +41 -0
  7. package/docs/index.md +31 -0
  8. package/docs/multi-dialect-support.md +34 -0
  9. package/docs/query-builder.md +75 -0
  10. package/docs/schema-definition.md +61 -0
  11. package/package.json +1 -1
  12. package/src/ast/expression.ts +433 -175
  13. package/src/ast/join.ts +8 -1
  14. package/src/ast/query.ts +64 -9
  15. package/src/builder/hydration-manager.ts +42 -11
  16. package/src/builder/hydration-planner.ts +80 -31
  17. package/src/builder/operations/column-selector.ts +37 -1
  18. package/src/builder/operations/cte-manager.ts +16 -0
  19. package/src/builder/operations/filter-manager.ts +32 -0
  20. package/src/builder/operations/join-manager.ts +17 -7
  21. package/src/builder/operations/pagination-manager.ts +19 -0
  22. package/src/builder/operations/relation-manager.ts +58 -3
  23. package/src/builder/query-ast-service.ts +100 -29
  24. package/src/builder/relation-conditions.ts +30 -1
  25. package/src/builder/relation-projection-helper.ts +43 -1
  26. package/src/builder/relation-service.ts +68 -13
  27. package/src/builder/relation-types.ts +6 -0
  28. package/src/builder/select-query-builder-deps.ts +64 -3
  29. package/src/builder/select-query-state.ts +72 -0
  30. package/src/builder/select.ts +166 -0
  31. package/src/codegen/typescript.ts +142 -44
  32. package/src/constants/sql-operator-config.ts +36 -0
  33. package/src/constants/sql.ts +125 -57
  34. package/src/dialect/abstract.ts +97 -22
  35. package/src/dialect/mssql/index.ts +27 -0
  36. package/src/dialect/mysql/index.ts +22 -0
  37. package/src/dialect/postgres/index.ts +103 -0
  38. package/src/dialect/sqlite/index.ts +22 -0
  39. package/src/runtime/als.ts +15 -1
  40. package/src/runtime/hydration.ts +20 -15
  41. package/src/schema/column.ts +45 -5
  42. package/src/schema/relation.ts +49 -2
  43. package/src/schema/table.ts +27 -3
  44. package/src/utils/join-node.ts +20 -0
  45. package/src/utils/raw-column-parser.ts +32 -0
  46. package/src/utils/relation-alias.ts +43 -0
  47. package/tests/postgres.test.ts +30 -0
  48. package/tests/window-function.test.ts +14 -0
@@ -1,14 +1,38 @@
1
1
  import { ColumnDef } from './column';
2
2
  import { RelationDef } from './relation';
3
3
 
4
+ /**
5
+ * Definition of a database table with its columns and relationships
6
+ * @typeParam T - Type of the columns record
7
+ */
4
8
  export interface TableDef<T extends Record<string, ColumnDef> = Record<string, ColumnDef>> {
9
+ /** Name of the table */
5
10
  name: string;
11
+ /** Record of column definitions keyed by column name */
6
12
  columns: T;
13
+ /** Record of relationship definitions keyed by relation name */
7
14
  relations: Record<string, RelationDef>;
8
15
  }
9
16
 
17
+ /**
18
+ * Creates a table definition with columns and relationships
19
+ * @typeParam T - Type of the columns record
20
+ * @param name - Name of the table
21
+ * @param columns - Record of column definitions
22
+ * @param relations - Record of relationship definitions (optional)
23
+ * @returns Complete table definition with runtime-filled column metadata
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const usersTable = defineTable('users', {
28
+ * id: col.primaryKey(col.int()),
29
+ * name: col.varchar(255),
30
+ * email: col.varchar(255)
31
+ * });
32
+ * ```
33
+ */
10
34
  export const defineTable = <T extends Record<string, ColumnDef>>(
11
- name: string,
35
+ name: string,
12
36
  columns: T,
13
37
  relations: Record<string, RelationDef> = {}
14
38
  ): TableDef<T> => {
@@ -17,6 +41,6 @@ export const defineTable = <T extends Record<string, ColumnDef>>(
17
41
  (acc as any)[key] = { ...def, name: key, table: name };
18
42
  return acc;
19
43
  }, {} as T);
20
-
44
+
21
45
  return { name, columns: colsWithNames, relations };
22
- };
46
+ };
@@ -0,0 +1,20 @@
1
+ import { JoinNode } from '../ast/join';
2
+ import { ExpressionNode } from '../ast/expression';
3
+ import { JoinKind } from '../constants/sql';
4
+
5
+ /**
6
+ * Creates a JoinNode ready for AST insertion.
7
+ * Centralizing this avoids copy/pasted object literals when multiple services need to synthesize joins.
8
+ */
9
+ export const createJoinNode = (
10
+ kind: JoinKind,
11
+ tableName: string,
12
+ condition: ExpressionNode,
13
+ relationName?: string
14
+ ): JoinNode => ({
15
+ type: 'Join',
16
+ kind,
17
+ table: { type: 'Table', name: tableName },
18
+ condition,
19
+ relationName
20
+ });
@@ -0,0 +1,32 @@
1
+ import { ColumnNode } from '../ast/expression';
2
+ import { CommonTableExpressionNode } from '../ast/query';
3
+
4
+ /**
5
+ * Best-effort helper that tries to convert a raw column expression into a `ColumnNode`.
6
+ * This parser is intentionally limited; use it only for simple references or function calls.
7
+ */
8
+ export const parseRawColumn = (
9
+ col: string,
10
+ tableName: string,
11
+ ctes?: CommonTableExpressionNode[]
12
+ ): ColumnNode => {
13
+ if (col.includes('(')) {
14
+ const [fn, rest] = col.split('(');
15
+ const colName = rest.replace(')', '');
16
+ const [table, name] = colName.includes('.') ? colName.split('.') : [tableName, colName];
17
+ return { type: 'Column', table, name, alias: col };
18
+ }
19
+
20
+ if (col.includes('.')) {
21
+ const [potentialCteName, columnName] = col.split('.');
22
+ const hasCte = ctes?.some(cte => cte.name === potentialCteName);
23
+
24
+ if (hasCte) {
25
+ return { type: 'Column', table: tableName, name: col };
26
+ }
27
+
28
+ return { type: 'Column', table: potentialCteName, name: columnName };
29
+ }
30
+
31
+ return { type: 'Column', table: tableName, name: col };
32
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Separator used when projecting relational columns
3
+ */
4
+ const RELATION_SEPARATOR = '__';
5
+
6
+ /**
7
+ * Parts of a relation alias
8
+ */
9
+ export interface RelationAliasParts {
10
+ /**
11
+ * Relation name (left side of the separator)
12
+ */
13
+ relationName: string;
14
+ /**
15
+ * Column name (right side of the separator)
16
+ */
17
+ columnName: string;
18
+ }
19
+
20
+ /**
21
+ * Builds a relation alias from the relation name and column name components.
22
+ */
23
+ export const makeRelationAlias = (relationName: string, columnName: string): string =>
24
+ `${relationName}${RELATION_SEPARATOR}${columnName}`;
25
+
26
+ /**
27
+ * Parses a relation alias into its relation/column components.
28
+ * Returns `null` when the alias does not follow the `relation__column` pattern.
29
+ */
30
+ export const parseRelationAlias = (alias: string): RelationAliasParts | null => {
31
+ const idx = alias.indexOf(RELATION_SEPARATOR);
32
+ if (idx === -1) return null;
33
+ return {
34
+ relationName: alias.slice(0, idx),
35
+ columnName: alias.slice(idx + RELATION_SEPARATOR.length)
36
+ };
37
+ };
38
+
39
+ /**
40
+ * Determines whether an alias represents a relation column by checking the `__` convention.
41
+ */
42
+ export const isRelationAlias = (alias?: string): boolean =>
43
+ !!alias && alias.includes(RELATION_SEPARATOR);
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SelectQueryBuilder } from '../src/builder/select';
3
+ import { PostgresDialect } from '../src/dialect/postgres';
4
+ import { Users } from '../src/playground/features/playground/data/schema';
5
+ import { jsonPath, eq } from '../src/ast/expression';
6
+
7
+ describe('PostgresDialect', () => {
8
+ it('should compile a simple select', () => {
9
+ const query = new SelectQueryBuilder(Users).selectRaw('*');
10
+ const dialect = new PostgresDialect();
11
+ const compiled = query.compile(dialect);
12
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users";');
13
+ });
14
+
15
+ it('should compile a select with a where clause', () => {
16
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(Users.columns.id, 1));
17
+ const dialect = new PostgresDialect();
18
+ const compiled = query.compile(dialect);
19
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."id" = ?;');
20
+ expect(compiled.params).toEqual([1]);
21
+ });
22
+
23
+ it('should compile a select with a json path', () => {
24
+ const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(jsonPath(Users.columns.settings, '$.first'), 'John'));
25
+ const dialect = new PostgresDialect();
26
+ const compiled = query.compile(dialect);
27
+ expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."settings"->>\'$.first\' = ?;');
28
+ expect(compiled.params).toEqual(['John']);
29
+ });
30
+ });
@@ -3,6 +3,7 @@ import { SelectQueryBuilder } from '../src/builder/select';
3
3
  import { SqliteDialect } from '../src/dialect/sqlite';
4
4
  import { MySqlDialect } from '../src/dialect/mysql';
5
5
  import { SqlServerDialect } from '../src/dialect/mssql';
6
+ import { PostgresDialect } from '../src/dialect/postgres';
6
7
  import { TableDef } from '../src/schema/table';
7
8
  import { rowNumber, rank, denseRank, lag, lead, ntile, firstValue, lastValue, windowFunction } from '../src/ast/expression';
8
9
 
@@ -14,6 +15,7 @@ describe('Window Function Support', () => {
14
15
  const sqlite = new SqliteDialect();
15
16
  const mysql = new MySqlDialect();
16
17
  const mssql = new SqlServerDialect();
18
+ const postgres = new PostgresDialect();
17
19
 
18
20
  it('should generate ROW_NUMBER() window function', () => {
19
21
  const users = table('users');
@@ -28,10 +30,12 @@ describe('Window Function Support', () => {
28
30
  const expectedSqlite = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
29
31
  const expectedMysql = 'SELECT `users`.`id` AS `id`, `users`.`name` AS `name`, ROW_NUMBER() OVER () AS `row_num` FROM `users`;';
30
32
  const expectedMssql = 'SELECT [users].[id] AS [id], [users].[name] AS [name], ROW_NUMBER() OVER () AS [row_num] FROM [users];';
33
+ const expectedPostgres = 'SELECT "users"."id" AS "id", "users"."name" AS "name", ROW_NUMBER() OVER () AS "row_num" FROM "users";';
31
34
 
32
35
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
33
36
  expect(query.toSql(mysql)).toBe(expectedMysql);
34
37
  expect(query.toSql(mssql)).toBe(expectedMssql);
38
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
35
39
  });
36
40
 
37
41
  it('should generate RANK() with PARTITION BY and ORDER BY', () => {
@@ -48,10 +52,12 @@ describe('Window Function Support', () => {
48
52
  const expectedSqlite = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
49
53
  const expectedMysql = 'SELECT `orders`.`id` AS `id`, `orders`.`customer_id` AS `customer_id`, `orders`.`amount` AS `amount`, RANK() OVER (PARTITION BY `orders`.`customer_id` ORDER BY `orders`.`amount` DESC) AS `rank` FROM `orders`;';
50
54
  const expectedMssql = 'SELECT [orders].[id] AS [id], [orders].[customer_id] AS [customer_id], [orders].[amount] AS [amount], RANK() OVER (PARTITION BY [orders].[customer_id] ORDER BY [orders].[amount] DESC) AS [rank] FROM [orders];';
55
+ const expectedPostgres = 'SELECT "orders"."id" AS "id", "orders"."customer_id" AS "customer_id", "orders"."amount" AS "amount", RANK() OVER (PARTITION BY "orders"."customer_id" ORDER BY "orders"."amount" DESC) AS "rank" FROM "orders";';
51
56
 
52
57
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
53
58
  expect(query.toSql(mysql)).toBe(expectedMysql);
54
59
  expect(query.toSql(mssql)).toBe(expectedMssql);
60
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
55
61
  });
56
62
 
57
63
  it('should generate LAG function with offset and default value', () => {
@@ -67,10 +73,12 @@ describe('Window Function Support', () => {
67
73
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
68
74
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LAG(`sales`.`amount`, ?, ?) OVER () AS `prev_amount` FROM `sales`;';
69
75
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LAG([sales].[amount], @p1, @p2) OVER () AS [prev_amount] FROM [sales];';
76
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LAG("sales"."amount", ?, ?) OVER () AS "prev_amount" FROM "sales";';
70
77
 
71
78
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
72
79
  expect(query.toSql(mysql)).toBe(expectedMysql);
73
80
  expect(query.toSql(mssql)).toBe(expectedMssql);
81
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
74
82
  });
75
83
 
76
84
  it('should generate LEAD function', () => {
@@ -86,10 +94,12 @@ describe('Window Function Support', () => {
86
94
  const expectedSqlite = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
87
95
  const expectedMysql = 'SELECT `sales`.`date` AS `date`, `sales`.`amount` AS `amount`, LEAD(`sales`.`amount`, ?) OVER () AS `next_amount` FROM `sales`;';
88
96
  const expectedMssql = 'SELECT [sales].[date] AS [date], [sales].[amount] AS [amount], LEAD([sales].[amount], @p1) OVER () AS [next_amount] FROM [sales];';
97
+ const expectedPostgres = 'SELECT "sales"."date" AS "date", "sales"."amount" AS "amount", LEAD("sales"."amount", ?) OVER () AS "next_amount" FROM "sales";';
89
98
 
90
99
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
91
100
  expect(query.toSql(mysql)).toBe(expectedMysql);
92
101
  expect(query.toSql(mssql)).toBe(expectedMssql);
102
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
93
103
  });
94
104
 
95
105
  it('should generate window function with both PARTITION BY and ORDER BY', () => {
@@ -107,10 +117,12 @@ describe('Window Function Support', () => {
107
117
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
108
118
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`department` AS `department`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER (PARTITION BY `employees`.`department` ORDER BY `employees`.`salary` DESC) AS `dept_rank` FROM `employees`;';
109
119
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[department] AS [department], [employees].[salary] AS [salary], ROW_NUMBER() OVER (PARTITION BY [employees].[department] ORDER BY [employees].[salary] DESC) AS [dept_rank] FROM [employees];';
120
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."department" AS "department", "employees"."salary" AS "salary", ROW_NUMBER() OVER (PARTITION BY "employees"."department" ORDER BY "employees"."salary" DESC) AS "dept_rank" FROM "employees";';
110
121
 
111
122
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
112
123
  expect(query.toSql(mysql)).toBe(expectedMysql);
113
124
  expect(query.toSql(mssql)).toBe(expectedMssql);
125
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
114
126
  });
115
127
 
116
128
  it('should generate multiple window functions in one query', () => {
@@ -129,9 +141,11 @@ describe('Window Function Support', () => {
129
141
  const expectedSqlite = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
130
142
  const expectedMysql = 'SELECT `employees`.`id` AS `id`, `employees`.`name` AS `name`, `employees`.`salary` AS `salary`, ROW_NUMBER() OVER () AS `row_num`, RANK() OVER () AS `rank`, DENSE_RANK() OVER () AS `dense_rank` FROM `employees`;';
131
143
  const expectedMssql = 'SELECT [employees].[id] AS [id], [employees].[name] AS [name], [employees].[salary] AS [salary], ROW_NUMBER() OVER () AS [row_num], RANK() OVER () AS [rank], DENSE_RANK() OVER () AS [dense_rank] FROM [employees];';
144
+ const expectedPostgres = 'SELECT "employees"."id" AS "id", "employees"."name" AS "name", "employees"."salary" AS "salary", ROW_NUMBER() OVER () AS "row_num", RANK() OVER () AS "rank", DENSE_RANK() OVER () AS "dense_rank" FROM "employees";';
132
145
 
133
146
  expect(query.toSql(sqlite)).toBe(expectedSqlite);
134
147
  expect(query.toSql(mysql)).toBe(expectedMysql);
135
148
  expect(query.toSql(mssql)).toBe(expectedMssql);
149
+ expect(query.toSql(postgres)).toBe(expectedPostgres);
136
150
  });
137
151
  });