schematic-pg 0.1.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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1019 -0
  3. package/dist/api/auth/errors.d.ts +6 -0
  4. package/dist/api/auth/errors.js +12 -0
  5. package/dist/api/auth/jwt-resolver.d.ts +7 -0
  6. package/dist/api/auth/jwt-resolver.js +59 -0
  7. package/dist/api/auth/middleware.d.ts +4 -0
  8. package/dist/api/auth/middleware.js +10 -0
  9. package/dist/api/auth/policy.d.ts +7 -0
  10. package/dist/api/auth/policy.js +95 -0
  11. package/dist/api/auth/template.d.ts +2 -0
  12. package/dist/api/auth/template.js +24 -0
  13. package/dist/api/auth/types.d.ts +12 -0
  14. package/dist/api/auth/types.js +1 -0
  15. package/dist/api/middleware/db.d.ts +8 -0
  16. package/dist/api/middleware/db.js +12 -0
  17. package/dist/api/middleware/errors.d.ts +5 -0
  18. package/dist/api/middleware/errors.js +27 -0
  19. package/dist/api/middleware/validate.d.ts +23 -0
  20. package/dist/api/middleware/validate.js +13 -0
  21. package/dist/api/types.d.ts +8 -0
  22. package/dist/api/types.js +1 -0
  23. package/dist/api/utils/route-naming.d.ts +3 -0
  24. package/dist/api/utils/route-naming.js +25 -0
  25. package/dist/api-generator/app-generator.d.ts +11 -0
  26. package/dist/api-generator/app-generator.js +79 -0
  27. package/dist/api-generator/custom-route-scanner.d.ts +6 -0
  28. package/dist/api-generator/custom-route-scanner.js +42 -0
  29. package/dist/api-generator/generate-api-cli.d.ts +1 -0
  30. package/dist/api-generator/generate-api-cli.js +28 -0
  31. package/dist/api-generator/index.d.ts +11 -0
  32. package/dist/api-generator/index.js +15 -0
  33. package/dist/api-generator/policy-generator.d.ts +9 -0
  34. package/dist/api-generator/policy-generator.js +33 -0
  35. package/dist/api-generator/route-generator.d.ts +20 -0
  36. package/dist/api-generator/route-generator.js +198 -0
  37. package/dist/api-generator/utils/policy.d.ts +18 -0
  38. package/dist/api-generator/utils/policy.js +72 -0
  39. package/dist/api-generator/zod-schema-generator.d.ts +16 -0
  40. package/dist/api-generator/zod-schema-generator.js +145 -0
  41. package/dist/cli/db.d.ts +4 -0
  42. package/dist/cli/db.js +144 -0
  43. package/dist/cli/dev.d.ts +1 -0
  44. package/dist/cli/dev.js +10 -0
  45. package/dist/cli/generate.d.ts +4 -0
  46. package/dist/cli/generate.js +50 -0
  47. package/dist/cli/init.d.ts +1 -0
  48. package/dist/cli/init.js +57 -0
  49. package/dist/cli/paths.d.ts +5 -0
  50. package/dist/cli/paths.js +10 -0
  51. package/dist/cli/templates.d.ts +6 -0
  52. package/dist/cli/templates.js +85 -0
  53. package/dist/cli.d.ts +2 -0
  54. package/dist/cli.js +81 -0
  55. package/dist/constants.d.ts +1 -0
  56. package/dist/constants.js +1 -0
  57. package/dist/db/bootstrap-cli.d.ts +1 -0
  58. package/dist/db/bootstrap-cli.js +17 -0
  59. package/dist/db/bootstrap.d.ts +3 -0
  60. package/dist/db/bootstrap.js +16 -0
  61. package/dist/db/cli.d.ts +1 -0
  62. package/dist/db/cli.js +20 -0
  63. package/dist/db/client.d.ts +8 -0
  64. package/dist/db/client.js +23 -0
  65. package/dist/db/config.d.ts +1 -0
  66. package/dist/db/config.js +10 -0
  67. package/dist/db/db-client-generator.d.ts +13 -0
  68. package/dist/db/db-client-generator.js +70 -0
  69. package/dist/db/diff-cli.d.ts +1 -0
  70. package/dist/db/diff-cli.js +46 -0
  71. package/dist/db/diff.d.ts +9 -0
  72. package/dist/db/diff.js +30 -0
  73. package/dist/db/errors.d.ts +34 -0
  74. package/dist/db/errors.js +88 -0
  75. package/dist/db/generate-client-cli.d.ts +1 -0
  76. package/dist/db/generate-client-cli.js +21 -0
  77. package/dist/db/index.d.ts +19 -0
  78. package/dist/db/index.js +17 -0
  79. package/dist/db/load-env.d.ts +3 -0
  80. package/dist/db/load-env.js +19 -0
  81. package/dist/db/migrate-cli.d.ts +1 -0
  82. package/dist/db/migrate-cli.js +88 -0
  83. package/dist/db/migrate.d.ts +3 -0
  84. package/dist/db/migrate.js +32 -0
  85. package/dist/db/migrations.d.ts +17 -0
  86. package/dist/db/migrations.js +81 -0
  87. package/dist/db/model-client.d.ts +36 -0
  88. package/dist/db/model-client.js +83 -0
  89. package/dist/db/model-meta.d.ts +36 -0
  90. package/dist/db/model-meta.js +57 -0
  91. package/dist/db/query-builder.d.ts +33 -0
  92. package/dist/db/query-builder.js +97 -0
  93. package/dist/db/reset-database.d.ts +4 -0
  94. package/dist/db/reset-database.js +9 -0
  95. package/dist/db/row-mapper.d.ts +3 -0
  96. package/dist/db/row-mapper.js +41 -0
  97. package/dist/db/schema-state.d.ts +7 -0
  98. package/dist/db/schema-state.js +32 -0
  99. package/dist/db/type-generator.d.ts +19 -0
  100. package/dist/db/type-generator.js +136 -0
  101. package/dist/db/utils/naming.d.ts +6 -0
  102. package/dist/db/utils/naming.js +23 -0
  103. package/dist/db/where-translator.d.ts +20 -0
  104. package/dist/db/where-translator.js +141 -0
  105. package/dist/index.d.ts +1 -0
  106. package/dist/index.js +1 -0
  107. package/dist/routes/health.d.ts +4 -0
  108. package/dist/routes/health.js +4 -0
  109. package/dist/schema-dsl/ast.d.ts +108 -0
  110. package/dist/schema-dsl/ast.js +1 -0
  111. package/dist/schema-dsl/cli.d.ts +1 -0
  112. package/dist/schema-dsl/cli.js +25 -0
  113. package/dist/schema-dsl/index.d.ts +8 -0
  114. package/dist/schema-dsl/index.js +16 -0
  115. package/dist/schema-dsl/inspect.d.ts +1 -0
  116. package/dist/schema-dsl/inspect.js +9 -0
  117. package/dist/schema-dsl/lexer.d.ts +31 -0
  118. package/dist/schema-dsl/lexer.js +216 -0
  119. package/dist/schema-dsl/parser.d.ts +49 -0
  120. package/dist/schema-dsl/parser.js +372 -0
  121. package/dist/schema-dsl/tokens.d.ts +30 -0
  122. package/dist/schema-dsl/tokens.js +35 -0
  123. package/dist/sql-generator/cli.d.ts +1 -0
  124. package/dist/sql-generator/cli.js +7 -0
  125. package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
  126. package/dist/sql-generator/generators/drop-tables.js +8 -0
  127. package/dist/sql-generator/generators/enums.d.ts +4 -0
  128. package/dist/sql-generator/generators/enums.js +16 -0
  129. package/dist/sql-generator/generators/extensions.d.ts +4 -0
  130. package/dist/sql-generator/generators/extensions.js +11 -0
  131. package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
  132. package/dist/sql-generator/generators/foreign-keys.js +23 -0
  133. package/dist/sql-generator/generators/indexes.d.ts +13 -0
  134. package/dist/sql-generator/generators/indexes.js +39 -0
  135. package/dist/sql-generator/generators/tables.d.ts +4 -0
  136. package/dist/sql-generator/generators/tables.js +65 -0
  137. package/dist/sql-generator/generators/triggers.d.ts +6 -0
  138. package/dist/sql-generator/generators/triggers.js +47 -0
  139. package/dist/sql-generator/index.d.ts +5 -0
  140. package/dist/sql-generator/index.js +3 -0
  141. package/dist/sql-generator/migration-planner.d.ts +15 -0
  142. package/dist/sql-generator/migration-planner.js +207 -0
  143. package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
  144. package/dist/sql-generator/migration-sql-generator.js +181 -0
  145. package/dist/sql-generator/migration-types.d.ts +86 -0
  146. package/dist/sql-generator/migration-types.js +1 -0
  147. package/dist/sql-generator/sql-generator.d.ts +6 -0
  148. package/dist/sql-generator/sql-generator.js +26 -0
  149. package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
  150. package/dist/sql-generator/utils/ast-helpers.js +252 -0
  151. package/dist/sql-generator/utils/format.d.ts +2 -0
  152. package/dist/sql-generator/utils/format.js +21 -0
  153. package/dist/sql-generator/utils/snake-case.d.ts +3 -0
  154. package/dist/sql-generator/utils/snake-case.js +96 -0
  155. package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
  156. package/dist/sql-generator/utils/type-mapper.js +39 -0
  157. package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
  158. package/dist/sql-generator/utils/value-formatter.js +41 -0
  159. package/dist/types/generated-db.stub.d.ts +2 -0
  160. package/dist/types/generated-db.stub.js +3 -0
  161. package/dist/types/generated-policies.stub.d.ts +7 -0
  162. package/dist/types/generated-policies.stub.js +1 -0
  163. package/package.json +86 -0
@@ -0,0 +1,83 @@
1
+ import { mapPgError } from './errors.js';
2
+ import { QueryBuilder } from './query-builder.js';
3
+ import { mapRow, mapRows } from './row-mapper.js';
4
+ export function createModelClient(model, pool) {
5
+ const builder = new QueryBuilder(model);
6
+ async function execute(sql, params) {
7
+ try {
8
+ const result = await pool.query(sql, params);
9
+ return result.rows;
10
+ }
11
+ catch (error) {
12
+ throw mapPgError(error, model.name, model.columnToField);
13
+ }
14
+ }
15
+ return {
16
+ async create(data) {
17
+ const query = builder.insert(data);
18
+ const rows = await execute(query.sql, query.params);
19
+ return mapRow(rows[0], model);
20
+ },
21
+ async findUnique(where) {
22
+ const query = builder.select({ where, take: 1 });
23
+ const rows = await execute(query.sql, query.params);
24
+ return rows[0] ? mapRow(rows[0], model) : null;
25
+ },
26
+ async findFirst(args) {
27
+ const query = builder.select({
28
+ where: args?.where,
29
+ orderBy: args?.orderBy,
30
+ take: 1,
31
+ });
32
+ const rows = await execute(query.sql, query.params);
33
+ return rows[0] ? mapRow(rows[0], model) : null;
34
+ },
35
+ async findMany(args) {
36
+ const query = builder.select({
37
+ where: args?.where,
38
+ orderBy: args?.orderBy,
39
+ take: args?.take,
40
+ skip: args?.skip,
41
+ });
42
+ const rows = await execute(query.sql, query.params);
43
+ return mapRows(rows, model);
44
+ },
45
+ async count(args) {
46
+ const query = builder.count({ where: args?.where });
47
+ const rows = await execute(query.sql, query.params);
48
+ return rows[0]?.count ?? 0;
49
+ },
50
+ async update(args) {
51
+ const query = builder.update({
52
+ where: args.where,
53
+ data: args.data,
54
+ });
55
+ const rows = await execute(query.sql, query.params);
56
+ if (!rows[0]) {
57
+ throw new Error(`Update returned no rows for model ${model.name}`);
58
+ }
59
+ return mapRow(rows[0], model);
60
+ },
61
+ async updateMany(args) {
62
+ const query = builder.update({
63
+ where: args.where,
64
+ data: args.data,
65
+ });
66
+ const rows = await execute(query.sql, query.params);
67
+ return { count: rows.length };
68
+ },
69
+ async delete(where) {
70
+ const query = builder.delete({ where: where });
71
+ const rows = await execute(query.sql, query.params);
72
+ if (!rows[0]) {
73
+ throw new Error(`Delete returned no rows for model ${model.name}`);
74
+ }
75
+ return mapRow(rows[0], model);
76
+ },
77
+ async deleteMany(args) {
78
+ const query = builder.delete({ where: args?.where });
79
+ const rows = await execute(query.sql, query.params);
80
+ return { count: rows.length };
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,36 @@
1
+ import type { Model, Schema, TypeExpr } from '../schema-dsl/ast.js';
2
+ export interface FieldMeta {
3
+ name: string;
4
+ columnName: string;
5
+ type: TypeExpr;
6
+ optional: boolean;
7
+ hasDefault: boolean;
8
+ isId: boolean;
9
+ isUnique: boolean;
10
+ isEnum: boolean;
11
+ isNumeric: boolean;
12
+ isString: boolean;
13
+ isBoolean: boolean;
14
+ }
15
+ export interface ModelMetaSnapshot {
16
+ name: string;
17
+ tableName: string;
18
+ quotedTableName: string;
19
+ primaryKeyFields: string[];
20
+ fields: FieldMeta[];
21
+ fieldByName: Record<string, FieldMeta>;
22
+ columnToField: Record<string, string>;
23
+ }
24
+ export interface ModelMeta {
25
+ name: string;
26
+ tableName: string;
27
+ quotedTableName: string;
28
+ primaryKeyFields: string[];
29
+ fields: FieldMeta[];
30
+ fieldByName: Map<string, FieldMeta>;
31
+ columnToField: Map<string, string>;
32
+ }
33
+ export declare function buildModelMeta(model: Model, schema: Schema): ModelMeta;
34
+ export declare function buildModelMetaSnapshot(model: Model, schema: Schema): ModelMetaSnapshot;
35
+ export declare function hydrateModelMeta(snapshot: ModelMetaSnapshot): ModelMeta;
36
+ export declare function buildModelMetas(schema: Schema): ModelMeta[];
@@ -0,0 +1,57 @@
1
+ import { fieldHasAttribute, getModelNames, getPrimaryKey, getStoredFields, } from '../sql-generator/utils/ast-helpers.js';
2
+ import { toColumnName, toTableName } from './utils/naming.js';
3
+ const NUMERIC_TYPES = new Set(['INTEGER', 'SERIAL', 'SMALLINT', 'DECIMAL']);
4
+ const STRING_TYPES = new Set(['UUID', 'VARCHAR', 'TEXT']);
5
+ export function buildModelMeta(model, schema) {
6
+ return hydrateModelMeta(buildModelMetaSnapshot(model, schema));
7
+ }
8
+ export function buildModelMetaSnapshot(model, schema) {
9
+ const modelNames = getModelNames(schema);
10
+ const enumNames = new Set(schema.enums.map((enumDef) => enumDef.name));
11
+ const primaryKey = getPrimaryKey(model);
12
+ const primaryKeyFields = primaryKey?.fields ?? ['id'];
13
+ const storedFields = getStoredFields(model, modelNames);
14
+ const fields = storedFields.map((field) => toFieldMeta(field, enumNames, primaryKeyFields));
15
+ const fieldByName = Object.fromEntries(fields.map((field) => [field.name, field]));
16
+ const columnToField = Object.fromEntries(fields.map((field) => [field.columnName, field.name]));
17
+ return {
18
+ name: model.name,
19
+ tableName: toTableName(model.name),
20
+ quotedTableName: quoteTable(model.name),
21
+ primaryKeyFields,
22
+ fields,
23
+ fieldByName,
24
+ columnToField,
25
+ };
26
+ }
27
+ export function hydrateModelMeta(snapshot) {
28
+ return {
29
+ ...snapshot,
30
+ fieldByName: new Map(Object.entries(snapshot.fieldByName)),
31
+ columnToField: new Map(Object.entries(snapshot.columnToField)),
32
+ };
33
+ }
34
+ export function buildModelMetas(schema) {
35
+ return schema.models.map((model) => buildModelMeta(model, schema));
36
+ }
37
+ function toFieldMeta(field, enumNames, primaryKeyFields) {
38
+ const typeName = field.type.name;
39
+ return {
40
+ name: field.name,
41
+ columnName: toColumnName(field.name),
42
+ type: field.type,
43
+ optional: Boolean(field.type.optional),
44
+ hasDefault: fieldHasAttribute(field, 'default') || fieldHasAttribute(field, 'id'),
45
+ isId: primaryKeyFields.includes(field.name) || fieldHasAttribute(field, 'id'),
46
+ isUnique: fieldHasAttribute(field, 'unique') || fieldHasAttribute(field, 'id'),
47
+ isEnum: enumNames.has(typeName),
48
+ isNumeric: NUMERIC_TYPES.has(typeName),
49
+ isString: STRING_TYPES.has(typeName) || enumNames.has(typeName),
50
+ isBoolean: typeName === 'BOOLEAN',
51
+ };
52
+ }
53
+ function quoteTable(modelName) {
54
+ const tableName = toTableName(modelName);
55
+ const reserved = new Set(['user', 'order']);
56
+ return reserved.has(tableName) ? `"${tableName}"` : tableName;
57
+ }
@@ -0,0 +1,33 @@
1
+ import type { ModelMeta } from './model-meta.js';
2
+ import { type WhereInput } from './where-translator.js';
3
+ export interface FindArgs {
4
+ where?: WhereInput;
5
+ orderBy?: OrderByInput | OrderByInput[];
6
+ take?: number;
7
+ skip?: number;
8
+ }
9
+ export interface UpdateArgs {
10
+ where?: WhereInput;
11
+ data: Record<string, unknown>;
12
+ }
13
+ export interface DeleteArgs {
14
+ where?: WhereInput;
15
+ }
16
+ export interface CountArgs {
17
+ where?: WhereInput;
18
+ }
19
+ export type OrderByInput = Record<string, 'asc' | 'desc'>;
20
+ export interface SqlQuery {
21
+ sql: string;
22
+ params: unknown[];
23
+ }
24
+ export declare class QueryBuilder {
25
+ private readonly model;
26
+ constructor(model: ModelMeta);
27
+ insert(data: Record<string, unknown>): SqlQuery;
28
+ select(args?: FindArgs): SqlQuery;
29
+ update(args: UpdateArgs): SqlQuery;
30
+ delete(args?: DeleteArgs): SqlQuery;
31
+ count(args?: CountArgs): SqlQuery;
32
+ private buildOrderBy;
33
+ }
@@ -0,0 +1,97 @@
1
+ import { WhereTranslator } from './where-translator.js';
2
+ export class QueryBuilder {
3
+ model;
4
+ constructor(model) {
5
+ this.model = model;
6
+ }
7
+ insert(data) {
8
+ const entries = this.model.fields.filter((field) => Object.prototype.hasOwnProperty.call(data, field.name));
9
+ if (entries.length === 0) {
10
+ throw new Error(`No insertable fields provided for model ${this.model.name}`);
11
+ }
12
+ const columns = entries.map((field) => field.columnName);
13
+ const values = entries.map((field) => data[field.name]);
14
+ const placeholders = values.map((_, index) => `$${index + 1}`);
15
+ const sql = `INSERT INTO ${this.model.quotedTableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
16
+ return { sql, params: values };
17
+ }
18
+ select(args = {}) {
19
+ const whereTranslator = new WhereTranslator(this.model);
20
+ const whereClause = whereTranslator.translate(args.where);
21
+ const orderByClause = this.buildOrderBy(args.orderBy);
22
+ const params = [...whereClause.params];
23
+ let sql = `SELECT * FROM ${this.model.quotedTableName}`;
24
+ if (whereClause.sql) {
25
+ sql += ` WHERE ${whereClause.sql}`;
26
+ }
27
+ if (orderByClause) {
28
+ sql += ` ${orderByClause}`;
29
+ }
30
+ if (args.take !== undefined) {
31
+ params.push(args.take);
32
+ sql += ` LIMIT $${params.length}`;
33
+ }
34
+ if (args.skip !== undefined) {
35
+ params.push(args.skip);
36
+ sql += ` OFFSET $${params.length}`;
37
+ }
38
+ return { sql, params };
39
+ }
40
+ update(args) {
41
+ const dataEntries = this.model.fields.filter((field) => Object.prototype.hasOwnProperty.call(args.data, field.name));
42
+ if (dataEntries.length === 0) {
43
+ throw new Error(`No updatable fields provided for model ${this.model.name}`);
44
+ }
45
+ const setClauses = [];
46
+ const params = [];
47
+ for (const field of dataEntries) {
48
+ params.push(args.data[field.name]);
49
+ setClauses.push(`${field.columnName} = $${params.length}`);
50
+ }
51
+ const whereTranslator = new WhereTranslator(this.model, params.length + 1);
52
+ const whereClause = whereTranslator.translate(args.where);
53
+ params.push(...whereClause.params);
54
+ let sql = `UPDATE ${this.model.quotedTableName} SET ${setClauses.join(', ')}`;
55
+ if (whereClause.sql) {
56
+ sql += ` WHERE ${whereClause.sql}`;
57
+ }
58
+ sql += ' RETURNING *';
59
+ return { sql, params };
60
+ }
61
+ delete(args = {}) {
62
+ const whereTranslator = new WhereTranslator(this.model);
63
+ const whereClause = whereTranslator.translate(args.where);
64
+ let sql = `DELETE FROM ${this.model.quotedTableName}`;
65
+ if (whereClause.sql) {
66
+ sql += ` WHERE ${whereClause.sql}`;
67
+ }
68
+ sql += ' RETURNING *';
69
+ return { sql, params: whereClause.params };
70
+ }
71
+ count(args = {}) {
72
+ const whereTranslator = new WhereTranslator(this.model);
73
+ const whereClause = whereTranslator.translate(args.where);
74
+ let sql = `SELECT COUNT(*)::int AS count FROM ${this.model.quotedTableName}`;
75
+ if (whereClause.sql) {
76
+ sql += ` WHERE ${whereClause.sql}`;
77
+ }
78
+ return { sql, params: whereClause.params };
79
+ }
80
+ buildOrderBy(orderBy) {
81
+ if (!orderBy) {
82
+ return '';
83
+ }
84
+ const entries = Array.isArray(orderBy) ? orderBy : [orderBy];
85
+ const parts = [];
86
+ for (const entry of entries) {
87
+ for (const [fieldName, direction] of Object.entries(entry)) {
88
+ const field = this.model.fieldByName.get(fieldName);
89
+ if (!field) {
90
+ throw new Error(`Unknown orderBy field "${fieldName}" on model ${this.model.name}`);
91
+ }
92
+ parts.push(`${field.columnName} ${direction.toUpperCase()}`);
93
+ }
94
+ }
95
+ return parts.length > 0 ? `ORDER BY ${parts.join(', ')}` : '';
96
+ }
97
+ }
@@ -0,0 +1,4 @@
1
+ import type { Pool, PoolClient } from 'pg';
2
+ type Queryable = Pick<Pool, 'query'> | Pick<PoolClient, 'query'>;
3
+ export declare function resetPublicSchema(client: Queryable): Promise<void>;
4
+ export {};
@@ -0,0 +1,9 @@
1
+ const RESET_SQL = `
2
+ DROP SCHEMA IF EXISTS public CASCADE;
3
+ CREATE SCHEMA public;
4
+ GRANT ALL ON SCHEMA public TO postgrest;
5
+ GRANT ALL ON SCHEMA public TO public;
6
+ `.trim();
7
+ export async function resetPublicSchema(client) {
8
+ await client.query(RESET_SQL);
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { ModelMeta } from './model-meta.js';
2
+ export declare function mapRow<T>(row: Record<string, unknown>, model: ModelMeta): T;
3
+ export declare function mapRows<T>(rows: Record<string, unknown>[], model: ModelMeta): T[];
@@ -0,0 +1,41 @@
1
+ import { toCamelCase } from './utils/naming.js';
2
+ export function mapRow(row, model) {
3
+ const mapped = {};
4
+ for (const [column, value] of Object.entries(row)) {
5
+ const fieldName = model.columnToField.get(column) ?? toCamelCase(column);
6
+ const field = model.fieldByName.get(fieldName);
7
+ mapped[fieldName] = field ? coerceValue(value, field) : value;
8
+ }
9
+ return mapped;
10
+ }
11
+ export function mapRows(rows, model) {
12
+ return rows.map((row) => mapRow(row, model));
13
+ }
14
+ function coerceValue(value, field) {
15
+ if (value === null || value === undefined) {
16
+ return null;
17
+ }
18
+ const typeName = field.type.name;
19
+ if (typeName === 'TIMESTAMP') {
20
+ return value instanceof Date ? value : new Date(String(value));
21
+ }
22
+ if (typeName === 'BOOLEAN') {
23
+ return Boolean(value);
24
+ }
25
+ if (typeName === 'JSONB') {
26
+ if (typeof value === 'string') {
27
+ return JSON.parse(value);
28
+ }
29
+ return value;
30
+ }
31
+ if (field.type.array && typeName === 'TEXT') {
32
+ return value;
33
+ }
34
+ if (typeName === 'DECIMAL') {
35
+ return typeof value === 'string' ? value : String(value);
36
+ }
37
+ if (field.isNumeric && typeof value === 'string') {
38
+ return Number(value);
39
+ }
40
+ return value;
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { Schema } from '../schema-dsl/ast.js';
2
+ export declare function getSnapshotPath(cwd?: string): string;
3
+ export declare function snapshotExists(cwd?: string): boolean;
4
+ export declare function readSnapshotSource(cwd?: string): string | null;
5
+ export declare function readSnapshotSchema(cwd?: string): Schema | null;
6
+ export declare function writeSnapshot(sourcePath: string, cwd?: string): void;
7
+ export declare function ensureSnapshot(schemaPath: string, cwd?: string): void;
@@ -0,0 +1,32 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { parse } from '../schema-dsl/index.js';
4
+ const SNAPSHOT_DIR = '.schema-state';
5
+ const SNAPSHOT_FILE = 'app.schema';
6
+ export function getSnapshotPath(cwd = process.cwd()) {
7
+ return join(cwd, SNAPSHOT_DIR, SNAPSHOT_FILE);
8
+ }
9
+ export function snapshotExists(cwd = process.cwd()) {
10
+ return existsSync(getSnapshotPath(cwd));
11
+ }
12
+ export function readSnapshotSource(cwd = process.cwd()) {
13
+ const snapshotPath = getSnapshotPath(cwd);
14
+ if (!existsSync(snapshotPath)) {
15
+ return null;
16
+ }
17
+ return readFileSync(snapshotPath, 'utf8');
18
+ }
19
+ export function readSnapshotSchema(cwd = process.cwd()) {
20
+ const source = readSnapshotSource(cwd);
21
+ return source ? parse(source) : null;
22
+ }
23
+ export function writeSnapshot(sourcePath, cwd = process.cwd()) {
24
+ const snapshotPath = getSnapshotPath(cwd);
25
+ mkdirSync(dirname(snapshotPath), { recursive: true });
26
+ copyFileSync(sourcePath, snapshotPath);
27
+ }
28
+ export function ensureSnapshot(schemaPath, cwd = process.cwd()) {
29
+ if (!snapshotExists(cwd)) {
30
+ writeSnapshot(schemaPath, cwd);
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import type { Schema } from '../schema-dsl/ast.js';
2
+ export declare class TypeGenerator {
3
+ private readonly schema;
4
+ constructor(schema: Schema);
5
+ generate(): string;
6
+ private generateEnumType;
7
+ private generateModelTypes;
8
+ private generateEntityInterface;
9
+ private generateCreateInput;
10
+ private generateUpdateInput;
11
+ private generateWhereInput;
12
+ private generateOrderByInput;
13
+ private toTsType;
14
+ private mapTypeExpr;
15
+ private isFilterable;
16
+ private toWhereFieldType;
17
+ }
18
+ export declare function generateDbTypes(schema: Schema): string;
19
+ export declare function getClientExportName(modelName: string): string;
@@ -0,0 +1,136 @@
1
+ import { fieldHasAttribute, getModelNames, getStoredFields, } from '../sql-generator/utils/ast-helpers.js';
2
+ import { toClientKey } from './utils/naming.js';
3
+ export class TypeGenerator {
4
+ schema;
5
+ constructor(schema) {
6
+ this.schema = schema;
7
+ }
8
+ generate() {
9
+ const enumBlocks = this.schema.enums.map((enumDef) => this.generateEnumType(enumDef.name, enumDef.values));
10
+ const modelBlocks = this.schema.models.flatMap((model) => this.generateModelTypes(model));
11
+ return [
12
+ '// Auto-generated by TypeGenerator. Do not edit manually.',
13
+ '',
14
+ ...enumBlocks,
15
+ ...modelBlocks,
16
+ ].join('\n');
17
+ }
18
+ generateEnumType(name, values) {
19
+ const union = values.map((value) => `'${value}'`).join(' | ');
20
+ return `export type ${name} = ${union};\n`;
21
+ }
22
+ generateModelTypes(model) {
23
+ const modelNames = getModelNames(this.schema);
24
+ const storedFields = getStoredFields(model, modelNames);
25
+ return [
26
+ this.generateEntityInterface(model.name, storedFields),
27
+ this.generateCreateInput(model.name, storedFields),
28
+ this.generateUpdateInput(model.name, storedFields),
29
+ this.generateWhereInput(model.name, storedFields),
30
+ this.generateOrderByInput(model.name, storedFields),
31
+ '',
32
+ ];
33
+ }
34
+ generateEntityInterface(modelName, fields) {
35
+ const lines = fields.map((field) => ` ${field.name}: ${this.toTsType(field, 'entity')};`);
36
+ return `export interface ${modelName} {\n${lines.join('\n')}\n}\n`;
37
+ }
38
+ generateCreateInput(modelName, fields) {
39
+ const lines = fields
40
+ .filter((field) => !fieldHasAttribute(field, 'id'))
41
+ .map((field) => {
42
+ const optionalMarker = field.type.optional || fieldHasAttribute(field, 'default') ? '?' : '';
43
+ return ` ${field.name}${optionalMarker}: ${this.toTsType(field, 'input')};`;
44
+ });
45
+ return `export interface ${modelName}CreateInput {\n${lines.join('\n')}\n}\n`;
46
+ }
47
+ generateUpdateInput(modelName, fields) {
48
+ const lines = fields
49
+ .filter((field) => !fieldHasAttribute(field, 'id'))
50
+ .map((field) => ` ${field.name}?: ${this.toTsType(field, 'input')};`);
51
+ return `export interface ${modelName}UpdateInput {\n${lines.join('\n')}\n}\n`;
52
+ }
53
+ generateWhereInput(modelName, fields) {
54
+ const filterableFields = fields.filter((field) => this.isFilterable(field));
55
+ const lines = filterableFields.map((field) => ` ${field.name}?: ${this.toWhereFieldType(field)};`);
56
+ lines.push(' AND?: ' + modelName + 'WhereInput[];');
57
+ lines.push(' OR?: ' + modelName + 'WhereInput[];');
58
+ lines.push(' NOT?: ' + modelName + 'WhereInput;');
59
+ return `export interface ${modelName}WhereInput {\n${lines.join('\n')}\n}\n`;
60
+ }
61
+ generateOrderByInput(modelName, fields) {
62
+ const lines = fields.map((field) => ` ${field.name}?: 'asc' | 'desc';`);
63
+ return `export interface ${modelName}OrderByInput {\n${lines.join('\n')}\n}\n`;
64
+ }
65
+ toTsType(field, mode) {
66
+ const base = this.mapTypeExpr(field.type);
67
+ if (field.type.optional) {
68
+ return `${base} | null`;
69
+ }
70
+ if (mode === 'input' && fieldHasAttribute(field, 'default')) {
71
+ return base;
72
+ }
73
+ return base;
74
+ }
75
+ mapTypeExpr(type) {
76
+ const enumType = this.schema.enums.find((enumDef) => enumDef.name === type.name);
77
+ if (enumType) {
78
+ return type.name;
79
+ }
80
+ if (type.array && type.name === 'TEXT') {
81
+ return 'string[]';
82
+ }
83
+ switch (type.name) {
84
+ case 'UUID':
85
+ case 'VARCHAR':
86
+ case 'TEXT':
87
+ return 'string';
88
+ case 'INTEGER':
89
+ case 'SERIAL':
90
+ case 'SMALLINT':
91
+ return 'number';
92
+ case 'BOOLEAN':
93
+ return 'boolean';
94
+ case 'TIMESTAMP':
95
+ return 'Date';
96
+ case 'DECIMAL':
97
+ return 'string';
98
+ case 'JSONB':
99
+ return 'Record<string, unknown>';
100
+ case 'POINT':
101
+ return 'unknown';
102
+ default:
103
+ return 'unknown';
104
+ }
105
+ }
106
+ isFilterable(field) {
107
+ const typeName = field.type.name;
108
+ return !this.schema.models.some((model) => model.name === typeName);
109
+ }
110
+ toWhereFieldType(field) {
111
+ const base = this.mapTypeExpr(field.type);
112
+ const scalar = field.type.optional ? `${base} | null` : base;
113
+ const filters = [`{ equals: ${scalar} }`];
114
+ const isStringType = ['UUID', 'VARCHAR', 'TEXT'].includes(field.type.name);
115
+ const isEnumType = this.schema.enums.some((enumDef) => enumDef.name === field.type.name);
116
+ if (isStringType) {
117
+ filters.push(`{ contains: string }`, `{ startsWith: string }`, `{ endsWith: string }`);
118
+ }
119
+ if (isEnumType) {
120
+ filters.push(`{ in: ${base}[] }`);
121
+ }
122
+ if (['INTEGER', 'SERIAL', 'SMALLINT', 'DECIMAL'].includes(field.type.name)) {
123
+ filters.push(`{ gt: ${base} }`, `{ gte: ${base} }`, `{ lt: ${base} }`, `{ lte: ${base} }`);
124
+ }
125
+ if (field.type.name === 'BOOLEAN') {
126
+ return scalar;
127
+ }
128
+ return [scalar, ...filters].join(' | ');
129
+ }
130
+ }
131
+ export function generateDbTypes(schema) {
132
+ return new TypeGenerator(schema).generate();
133
+ }
134
+ export function getClientExportName(modelName) {
135
+ return toClientKey(modelName);
136
+ }
@@ -0,0 +1,6 @@
1
+ export { quoteIdentifier, toSnakeCase } from '../../sql-generator/utils/snake-case.js';
2
+ export declare function toCamelCase(value: string): string;
3
+ export declare function pluralize(value: string): string;
4
+ export declare function toTableName(modelName: string): string;
5
+ export declare function toColumnName(fieldName: string): string;
6
+ export declare function toClientKey(modelName: string): string;
@@ -0,0 +1,23 @@
1
+ export { quoteIdentifier, toSnakeCase } from '../../sql-generator/utils/snake-case.js';
2
+ import { toSnakeCase } from '../../sql-generator/utils/snake-case.js';
3
+ export function toCamelCase(value) {
4
+ return value.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
5
+ }
6
+ export function pluralize(value) {
7
+ if (/[^aeiou]y$/i.test(value)) {
8
+ return `${value.slice(0, -1)}ies`;
9
+ }
10
+ if (/(s|x|z|ch|sh)$/i.test(value)) {
11
+ return `${value}es`;
12
+ }
13
+ return `${value}s`;
14
+ }
15
+ export function toTableName(modelName) {
16
+ return toSnakeCase(modelName);
17
+ }
18
+ export function toColumnName(fieldName) {
19
+ return toSnakeCase(fieldName);
20
+ }
21
+ export function toClientKey(modelName) {
22
+ return modelName.charAt(0).toLowerCase() + modelName.slice(1);
23
+ }
@@ -0,0 +1,20 @@
1
+ import type { ModelMeta } from './model-meta.js';
2
+ export type WhereInput = Record<string, unknown>;
3
+ export interface WhereClause {
4
+ sql: string;
5
+ params: unknown[];
6
+ }
7
+ export declare class WhereTranslator {
8
+ private readonly model;
9
+ private readonly scalarFields;
10
+ private paramIndex;
11
+ private readonly startParamIndex;
12
+ constructor(model: ModelMeta, startParamIndex?: number);
13
+ translate(where: WhereInput | undefined): WhereClause;
14
+ getNextParamIndex(): number;
15
+ private translateLogical;
16
+ private translateNested;
17
+ private translateField;
18
+ private translateOperator;
19
+ private nextPlaceholder;
20
+ }