turbine-orm 0.4.0 → 0.7.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.
- package/README.md +243 -26
- package/dist/cjs/cli/config.js +151 -0
- package/dist/cjs/cli/index.js +1176 -0
- package/dist/cjs/cli/migrate.js +446 -0
- package/dist/cjs/cli/ui.js +233 -0
- package/dist/cjs/client.js +512 -0
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +321 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/introspect.js +287 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +78 -0
- package/dist/cjs/query.js +1891 -0
- package/dist/cjs/schema-builder.js +238 -0
- package/dist/cjs/schema-sql.js +509 -0
- package/dist/cjs/schema.js +140 -0
- package/dist/cjs/serverless.js +110 -0
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +256 -49
- package/dist/cli/migrate.d.ts +35 -6
- package/dist/cli/migrate.js +124 -76
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +87 -3
- package/dist/client.js +122 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.js +37 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.js +15 -11
- package/dist/introspect.js +3 -5
- package/dist/pipeline.js +8 -1
- package/dist/query.d.ts +310 -45
- package/dist/query.js +565 -237
- package/dist/schema-builder.js +91 -23
- package/dist/schema-sql.d.ts +6 -2
- package/dist/schema-sql.js +180 -26
- package/dist/schema.js +4 -1
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +44 -21
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -126
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm — Error types
|
|
4
|
+
*
|
|
5
|
+
* Typed errors with error codes for programmatic handling.
|
|
6
|
+
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CheckConstraintError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
10
|
+
exports.wrapPgError = wrapPgError;
|
|
11
|
+
/** Error codes for all Turbine errors */
|
|
12
|
+
exports.TurbineErrorCode = {
|
|
13
|
+
NOT_FOUND: 'TURBINE_E001',
|
|
14
|
+
TIMEOUT: 'TURBINE_E002',
|
|
15
|
+
VALIDATION: 'TURBINE_E003',
|
|
16
|
+
CONNECTION: 'TURBINE_E004',
|
|
17
|
+
RELATION: 'TURBINE_E005',
|
|
18
|
+
MIGRATION: 'TURBINE_E006',
|
|
19
|
+
CIRCULAR_RELATION: 'TURBINE_E007',
|
|
20
|
+
UNIQUE_VIOLATION: 'TURBINE_E008',
|
|
21
|
+
FOREIGN_KEY_VIOLATION: 'TURBINE_E009',
|
|
22
|
+
NOT_NULL_VIOLATION: 'TURBINE_E010',
|
|
23
|
+
CHECK_VIOLATION: 'TURBINE_E011',
|
|
24
|
+
};
|
|
25
|
+
/** Base error class for all Turbine errors */
|
|
26
|
+
class TurbineError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
constructor(code, message, options) {
|
|
29
|
+
super(message, options);
|
|
30
|
+
this.name = 'TurbineError';
|
|
31
|
+
this.code = code;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.TurbineError = TurbineError;
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
|
|
37
|
+
* update/delete against a non-matching row, etc.)
|
|
38
|
+
*
|
|
39
|
+
* Supports two call styles for back-compat:
|
|
40
|
+
* - `new NotFoundError()` / `new NotFoundError('custom message')`
|
|
41
|
+
* - `new NotFoundError({ table, where, operation, cause, message })`
|
|
42
|
+
*
|
|
43
|
+
* When called with an options object and no explicit `message`, a Prisma-style
|
|
44
|
+
* message is built automatically, e.g.:
|
|
45
|
+
* `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
|
|
46
|
+
*/
|
|
47
|
+
class NotFoundError extends TurbineError {
|
|
48
|
+
table;
|
|
49
|
+
where;
|
|
50
|
+
operation;
|
|
51
|
+
constructor(input) {
|
|
52
|
+
// Back-compat: string argument (or undefined) — replicate legacy behavior.
|
|
53
|
+
if (typeof input === 'string' || input === undefined) {
|
|
54
|
+
super(exports.TurbineErrorCode.NOT_FOUND, input ?? 'Record not found');
|
|
55
|
+
this.name = 'NotFoundError';
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const { table, where, operation, cause } = input;
|
|
59
|
+
let message = input.message;
|
|
60
|
+
if (!message) {
|
|
61
|
+
if (operation && table) {
|
|
62
|
+
const wherePart = where !== undefined ? ` matching where: ${JSON.stringify(where)}` : '';
|
|
63
|
+
message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
|
|
64
|
+
}
|
|
65
|
+
else if (table) {
|
|
66
|
+
const wherePart = where !== undefined ? ` matching where ${JSON.stringify(where)}` : '';
|
|
67
|
+
message = `[turbine] No record found in "${table}"${wherePart}`;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
message = '[turbine] Record not found';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
super(exports.TurbineErrorCode.NOT_FOUND, message, { cause });
|
|
74
|
+
this.name = 'NotFoundError';
|
|
75
|
+
this.table = table;
|
|
76
|
+
this.where = where;
|
|
77
|
+
this.operation = operation;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.NotFoundError = NotFoundError;
|
|
81
|
+
/** Thrown when a query or transaction exceeds the configured timeout */
|
|
82
|
+
class TimeoutError extends TurbineError {
|
|
83
|
+
timeoutMs;
|
|
84
|
+
constructor(timeoutMs, context = 'Query') {
|
|
85
|
+
super(exports.TurbineErrorCode.TIMEOUT, `[turbine] ${context} timed out after ${timeoutMs}ms`);
|
|
86
|
+
this.name = 'TimeoutError';
|
|
87
|
+
this.timeoutMs = timeoutMs;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.TimeoutError = TimeoutError;
|
|
91
|
+
/** Thrown when query arguments fail validation (unknown column, invalid operator, etc.) */
|
|
92
|
+
class ValidationError extends TurbineError {
|
|
93
|
+
constructor(message) {
|
|
94
|
+
super(exports.TurbineErrorCode.VALIDATION, message);
|
|
95
|
+
this.name = 'ValidationError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.ValidationError = ValidationError;
|
|
99
|
+
/** Thrown when a database connection fails */
|
|
100
|
+
class ConnectionError extends TurbineError {
|
|
101
|
+
constructor(message) {
|
|
102
|
+
super(exports.TurbineErrorCode.CONNECTION, message);
|
|
103
|
+
this.name = 'ConnectionError';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.ConnectionError = ConnectionError;
|
|
107
|
+
/** Thrown when a relation reference is invalid */
|
|
108
|
+
class RelationError extends TurbineError {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(exports.TurbineErrorCode.RELATION, message);
|
|
111
|
+
this.name = 'RelationError';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.RelationError = RelationError;
|
|
115
|
+
/** Thrown when a migration operation fails */
|
|
116
|
+
class MigrationError extends TurbineError {
|
|
117
|
+
constructor(message) {
|
|
118
|
+
super(exports.TurbineErrorCode.MIGRATION, message);
|
|
119
|
+
this.name = 'MigrationError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.MigrationError = MigrationError;
|
|
123
|
+
/** Thrown when circular relation nesting is detected */
|
|
124
|
+
class CircularRelationError extends TurbineError {
|
|
125
|
+
path;
|
|
126
|
+
constructor(path) {
|
|
127
|
+
super(exports.TurbineErrorCode.CIRCULAR_RELATION, `[turbine] Circular or too-deep relation nesting detected: ${path.join(' → ')}. Maximum nesting depth is 10.`);
|
|
128
|
+
this.name = 'CircularRelationError';
|
|
129
|
+
this.path = path;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.CircularRelationError = CircularRelationError;
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Database constraint violation errors
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Extract the `detail` string from a pg-style error stored as `cause`.
|
|
138
|
+
* Returns undefined if the cause is not an object or has no detail.
|
|
139
|
+
*/
|
|
140
|
+
function detailFromCause(cause) {
|
|
141
|
+
if (!cause || typeof cause !== 'object')
|
|
142
|
+
return undefined;
|
|
143
|
+
const d = cause.detail;
|
|
144
|
+
return typeof d === 'string' && d.length > 0 ? d : undefined;
|
|
145
|
+
}
|
|
146
|
+
/** Thrown when a UNIQUE constraint is violated (pg code 23505) */
|
|
147
|
+
class UniqueConstraintError extends TurbineError {
|
|
148
|
+
constraint;
|
|
149
|
+
columns;
|
|
150
|
+
table;
|
|
151
|
+
constructor(opts = {}) {
|
|
152
|
+
const { constraint, columns, table, cause } = opts;
|
|
153
|
+
let message = opts.message;
|
|
154
|
+
if (!message) {
|
|
155
|
+
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
156
|
+
const columnsPart = columns && columns.length > 0 ? ` (${columns.join(', ')})` : '';
|
|
157
|
+
message = `[turbine] Unique constraint violation${constraintPart}${columnsPart}`;
|
|
158
|
+
const detail = detailFromCause(cause);
|
|
159
|
+
if (detail)
|
|
160
|
+
message += `: ${detail}`;
|
|
161
|
+
}
|
|
162
|
+
super(exports.TurbineErrorCode.UNIQUE_VIOLATION, message, { cause });
|
|
163
|
+
this.name = 'UniqueConstraintError';
|
|
164
|
+
this.constraint = constraint;
|
|
165
|
+
this.columns = columns;
|
|
166
|
+
this.table = table;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.UniqueConstraintError = UniqueConstraintError;
|
|
170
|
+
/** Thrown when a FOREIGN KEY constraint is violated (pg code 23503) */
|
|
171
|
+
class ForeignKeyError extends TurbineError {
|
|
172
|
+
constraint;
|
|
173
|
+
table;
|
|
174
|
+
constructor(opts = {}) {
|
|
175
|
+
const { constraint, table, cause } = opts;
|
|
176
|
+
let message = opts.message;
|
|
177
|
+
if (!message) {
|
|
178
|
+
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
179
|
+
message = `[turbine] Foreign key constraint violation${constraintPart}`;
|
|
180
|
+
const detail = detailFromCause(cause);
|
|
181
|
+
if (detail)
|
|
182
|
+
message += `: ${detail}`;
|
|
183
|
+
}
|
|
184
|
+
super(exports.TurbineErrorCode.FOREIGN_KEY_VIOLATION, message, { cause });
|
|
185
|
+
this.name = 'ForeignKeyError';
|
|
186
|
+
this.constraint = constraint;
|
|
187
|
+
this.table = table;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.ForeignKeyError = ForeignKeyError;
|
|
191
|
+
/** Thrown when a NOT NULL constraint is violated (pg code 23502) */
|
|
192
|
+
class NotNullViolationError extends TurbineError {
|
|
193
|
+
column;
|
|
194
|
+
table;
|
|
195
|
+
constructor(opts = {}) {
|
|
196
|
+
const { column, table, cause } = opts;
|
|
197
|
+
let message = opts.message;
|
|
198
|
+
if (!message) {
|
|
199
|
+
const columnPart = column ? ` on column "${column}"` : '';
|
|
200
|
+
message = `[turbine] NOT NULL constraint violation${columnPart}`;
|
|
201
|
+
const detail = detailFromCause(cause);
|
|
202
|
+
if (detail)
|
|
203
|
+
message += `: ${detail}`;
|
|
204
|
+
}
|
|
205
|
+
super(exports.TurbineErrorCode.NOT_NULL_VIOLATION, message, { cause });
|
|
206
|
+
this.name = 'NotNullViolationError';
|
|
207
|
+
this.column = column;
|
|
208
|
+
this.table = table;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.NotNullViolationError = NotNullViolationError;
|
|
212
|
+
/** Thrown when a CHECK constraint is violated (pg code 23514) */
|
|
213
|
+
class CheckConstraintError extends TurbineError {
|
|
214
|
+
constraint;
|
|
215
|
+
table;
|
|
216
|
+
constructor(opts = {}) {
|
|
217
|
+
const { constraint, table, cause } = opts;
|
|
218
|
+
let message = opts.message;
|
|
219
|
+
if (!message) {
|
|
220
|
+
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
221
|
+
message = `[turbine] Check constraint violation${constraintPart}`;
|
|
222
|
+
const detail = detailFromCause(cause);
|
|
223
|
+
if (detail)
|
|
224
|
+
message += `: ${detail}`;
|
|
225
|
+
}
|
|
226
|
+
super(exports.TurbineErrorCode.CHECK_VIOLATION, message, { cause });
|
|
227
|
+
this.name = 'CheckConstraintError';
|
|
228
|
+
this.constraint = constraint;
|
|
229
|
+
this.table = table;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.CheckConstraintError = CheckConstraintError;
|
|
233
|
+
/**
|
|
234
|
+
* Parse column names out of a pg `detail` string like:
|
|
235
|
+
* "Key (email)=(foo@bar) already exists."
|
|
236
|
+
* "Key (col1, col2)=(v1, v2) already exists."
|
|
237
|
+
*/
|
|
238
|
+
function parseColumnsFromDetail(detail) {
|
|
239
|
+
const m = detail.match(/^Key \(([^)]+)\)/);
|
|
240
|
+
if (!m)
|
|
241
|
+
return undefined;
|
|
242
|
+
return m[1].split(',').map((s) => s.trim());
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Translate a pg driver error into a typed Turbine error.
|
|
246
|
+
* If the error doesn't match a known constraint code, returns it unchanged.
|
|
247
|
+
*
|
|
248
|
+
* Maps:
|
|
249
|
+
* 23505 (unique_violation) -> UniqueConstraintError
|
|
250
|
+
* 23503 (foreign_key_violation) -> ForeignKeyError
|
|
251
|
+
* 23502 (not_null_violation) -> NotNullViolationError
|
|
252
|
+
* 23514 (check_violation) -> CheckConstraintError
|
|
253
|
+
*
|
|
254
|
+
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
255
|
+
*/
|
|
256
|
+
function wrapPgError(err) {
|
|
257
|
+
if (!err || typeof err !== 'object')
|
|
258
|
+
return err;
|
|
259
|
+
const e = err;
|
|
260
|
+
if (!e.code)
|
|
261
|
+
return err;
|
|
262
|
+
switch (e.code) {
|
|
263
|
+
case '23505': {
|
|
264
|
+
const cols = e.detail ? parseColumnsFromDetail(e.detail) : undefined;
|
|
265
|
+
return new UniqueConstraintError({
|
|
266
|
+
constraint: e.constraint,
|
|
267
|
+
columns: cols,
|
|
268
|
+
table: e.table,
|
|
269
|
+
cause: err,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
case '23503':
|
|
273
|
+
return new ForeignKeyError({
|
|
274
|
+
constraint: e.constraint,
|
|
275
|
+
table: e.table,
|
|
276
|
+
cause: err,
|
|
277
|
+
});
|
|
278
|
+
case '23502':
|
|
279
|
+
return new NotNullViolationError({
|
|
280
|
+
column: e.column,
|
|
281
|
+
table: e.table,
|
|
282
|
+
cause: err,
|
|
283
|
+
});
|
|
284
|
+
case '23514':
|
|
285
|
+
return new CheckConstraintError({
|
|
286
|
+
constraint: e.constraint,
|
|
287
|
+
table: e.table,
|
|
288
|
+
cause: err,
|
|
289
|
+
});
|
|
290
|
+
default:
|
|
291
|
+
return err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm — Code generator
|
|
4
|
+
*
|
|
5
|
+
* Takes an IntrospectedSchema and emits TypeScript files:
|
|
6
|
+
* - types.ts — Entity interfaces, Create/Update input types
|
|
7
|
+
* - metadata.ts — Runtime schema metadata (column maps, relations, etc.)
|
|
8
|
+
* - index.ts — Configured TurbineClient with typed table accessors
|
|
9
|
+
*
|
|
10
|
+
* Output goes to the specified directory (default: ./generated/turbine/).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.generate = generate;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const schema_js_1 = require("./schema.js");
|
|
17
|
+
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
18
|
+
function entityName(tableName) {
|
|
19
|
+
return (0, schema_js_1.snakeToPascal)((0, schema_js_1.singularize)(tableName));
|
|
20
|
+
}
|
|
21
|
+
/** Escape a value for embedding in a single-quoted TypeScript string literal */
|
|
22
|
+
function escSQ(value) {
|
|
23
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Main generate function
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function generate(options) {
|
|
29
|
+
const outDir = options.outDir ?? './generated/turbine';
|
|
30
|
+
// Path traversal protection — ensure output stays within project root
|
|
31
|
+
const resolved = (0, node_path_1.resolve)(outDir);
|
|
32
|
+
const rel = (0, node_path_1.relative)(process.cwd(), resolved);
|
|
33
|
+
if (rel.startsWith('..') || (0, node_path_1.resolve)(rel) !== resolved) {
|
|
34
|
+
throw new Error(`Output directory must be within the project root. Got: ${outDir}`);
|
|
35
|
+
}
|
|
36
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
37
|
+
const files = [];
|
|
38
|
+
// Generate types.ts
|
|
39
|
+
const typesContent = generateTypes(options.schema);
|
|
40
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'types.ts'), typesContent, 'utf-8');
|
|
41
|
+
files.push('types.ts');
|
|
42
|
+
// Generate metadata.ts
|
|
43
|
+
const metadataContent = generateMetadata(options.schema);
|
|
44
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'metadata.ts'), metadataContent, 'utf-8');
|
|
45
|
+
files.push('metadata.ts');
|
|
46
|
+
// Generate index.ts (configured client)
|
|
47
|
+
const indexContent = generateIndex(options.schema);
|
|
48
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
49
|
+
files.push('index.ts');
|
|
50
|
+
return { outDir, files };
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// types.ts generator
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
function generatedFileHeader() {
|
|
56
|
+
return [
|
|
57
|
+
'/**',
|
|
58
|
+
' * Auto-generated by turbine-orm — DO NOT EDIT',
|
|
59
|
+
' *',
|
|
60
|
+
` * Generated at: ${new Date().toISOString()}`,
|
|
61
|
+
' * @see https://turbineorm.dev',
|
|
62
|
+
' */',
|
|
63
|
+
'',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function generateTypes(schema) {
|
|
67
|
+
const lines = [...generatedFileHeader()];
|
|
68
|
+
// Generate enum types
|
|
69
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
70
|
+
const typeName = (0, schema_js_1.snakeToPascal)(enumName);
|
|
71
|
+
lines.push(`/** Database enum: ${enumName} */`);
|
|
72
|
+
lines.push(`export type ${typeName} = ${labels.map((l) => `'${escSQ(l)}'`).join(' | ')};`);
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
// Generate entity types for each table
|
|
76
|
+
for (const table of Object.values(schema.tables)) {
|
|
77
|
+
const typeName = entityName(table.name);
|
|
78
|
+
// --- Base entity interface ---
|
|
79
|
+
lines.push(`/** Row type for the \`${table.name}\` table */`);
|
|
80
|
+
lines.push(`export interface ${typeName} {`);
|
|
81
|
+
for (const col of table.columns) {
|
|
82
|
+
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
83
|
+
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
84
|
+
lines.push(` /** Column: ${col.name} — ${col.pgType}${pkNote}${nullNote} */`);
|
|
85
|
+
lines.push(` ${col.field}: ${col.tsType};`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('}');
|
|
88
|
+
lines.push('');
|
|
89
|
+
// --- Create input type ---
|
|
90
|
+
// Required: non-nullable columns without defaults (except PK)
|
|
91
|
+
// Optional: nullable columns (default to NULL) or columns with explicit defaults
|
|
92
|
+
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
93
|
+
lines.push(`export type ${typeName}Create = {`);
|
|
94
|
+
for (const col of table.columns) {
|
|
95
|
+
const isPk = table.primaryKey.includes(col.name);
|
|
96
|
+
const isOptional = col.hasDefault || col.nullable || isPk;
|
|
97
|
+
if (isOptional) {
|
|
98
|
+
const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
99
|
+
lines.push(` /** Optional: ${reason} */`);
|
|
100
|
+
lines.push(` ${col.field}?: ${col.tsType};`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
lines.push(` ${col.field}: ${col.tsType};`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push('};');
|
|
107
|
+
lines.push('');
|
|
108
|
+
// --- Update input type (all fields optional except PK) ---
|
|
109
|
+
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
|
|
110
|
+
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
111
|
+
lines.push(`export type ${typeName}Update = {`);
|
|
112
|
+
for (const col of nonPkCols) {
|
|
113
|
+
lines.push(` ${col.field}?: ${col.tsType};`);
|
|
114
|
+
}
|
|
115
|
+
lines.push('};');
|
|
116
|
+
lines.push('');
|
|
117
|
+
// --- Relations map (for type-safe `with` clauses) ---
|
|
118
|
+
const hasRelations = Object.keys(table.relations).length > 0;
|
|
119
|
+
if (hasRelations) {
|
|
120
|
+
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
121
|
+
lines.push(`export interface ${typeName}Relations {`);
|
|
122
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
123
|
+
const targetType = entityName(rel.to);
|
|
124
|
+
if (rel.type === 'hasMany') {
|
|
125
|
+
lines.push(` ${relName}: ${targetType}[];`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
lines.push(` ${relName}: ${targetType} | null;`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
lines.push('}');
|
|
132
|
+
lines.push('');
|
|
133
|
+
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
134
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
135
|
+
const targetType = entityName(rel.to);
|
|
136
|
+
if (rel.type === 'hasMany') {
|
|
137
|
+
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
138
|
+
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
139
|
+
lines.push(` ${relName}: ${targetType}[];`);
|
|
140
|
+
lines.push('}');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
144
|
+
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
145
|
+
lines.push(` ${relName}: ${targetType} | null;`);
|
|
146
|
+
lines.push('}');
|
|
147
|
+
}
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// metadata.ts generator
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
function generateMetadata(schema) {
|
|
158
|
+
const lines = [
|
|
159
|
+
...generatedFileHeader(),
|
|
160
|
+
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
161
|
+
'',
|
|
162
|
+
'export const SCHEMA: SchemaMetadata = {',
|
|
163
|
+
' tables: {',
|
|
164
|
+
];
|
|
165
|
+
for (const table of Object.values(schema.tables)) {
|
|
166
|
+
lines.push(` ${table.name}: {`);
|
|
167
|
+
lines.push(` name: '${escSQ(table.name)}',`);
|
|
168
|
+
// columns
|
|
169
|
+
lines.push(' columns: [');
|
|
170
|
+
for (const col of table.columns) {
|
|
171
|
+
lines.push(` ${serializeColumn(col)},`);
|
|
172
|
+
}
|
|
173
|
+
lines.push(' ],');
|
|
174
|
+
// columnMap
|
|
175
|
+
lines.push(' columnMap: {');
|
|
176
|
+
for (const [field, col] of Object.entries(table.columnMap)) {
|
|
177
|
+
lines.push(` ${field}: '${escSQ(col)}',`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(' },');
|
|
180
|
+
// reverseColumnMap
|
|
181
|
+
lines.push(' reverseColumnMap: {');
|
|
182
|
+
for (const [col, field] of Object.entries(table.reverseColumnMap)) {
|
|
183
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(field)}',`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(' },');
|
|
186
|
+
// dateColumns
|
|
187
|
+
const dateCols = [...table.dateColumns];
|
|
188
|
+
lines.push(` dateColumns: new Set([${dateCols.map((c) => `'${escSQ(c)}'`).join(', ')}]),`);
|
|
189
|
+
// pgTypes
|
|
190
|
+
lines.push(' pgTypes: {');
|
|
191
|
+
for (const [col, pgType] of Object.entries(table.pgTypes)) {
|
|
192
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
|
|
193
|
+
}
|
|
194
|
+
lines.push(' },');
|
|
195
|
+
// allColumns
|
|
196
|
+
lines.push(` allColumns: [${table.allColumns.map((c) => `'${escSQ(c)}'`).join(', ')}],`);
|
|
197
|
+
// primaryKey
|
|
198
|
+
lines.push(` primaryKey: [${table.primaryKey.map((c) => `'${escSQ(c)}'`).join(', ')}],`);
|
|
199
|
+
// uniqueColumns
|
|
200
|
+
lines.push(` uniqueColumns: [${table.uniqueColumns.map((uc) => `[${uc.map((c) => `'${escSQ(c)}'`).join(', ')}]`).join(', ')}],`);
|
|
201
|
+
// relations
|
|
202
|
+
lines.push(' relations: {');
|
|
203
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
204
|
+
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: '${escSQ(rel.foreignKey)}', referenceKey: '${escSQ(rel.referenceKey)}' },`);
|
|
205
|
+
}
|
|
206
|
+
lines.push(' },');
|
|
207
|
+
// indexes
|
|
208
|
+
lines.push(' indexes: [');
|
|
209
|
+
for (const idx of table.indexes) {
|
|
210
|
+
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
211
|
+
}
|
|
212
|
+
lines.push(' ],');
|
|
213
|
+
lines.push(' },');
|
|
214
|
+
}
|
|
215
|
+
lines.push(' },');
|
|
216
|
+
// enums
|
|
217
|
+
lines.push(' enums: {');
|
|
218
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
219
|
+
lines.push(` ${enumName}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
220
|
+
}
|
|
221
|
+
lines.push(' },');
|
|
222
|
+
lines.push('};');
|
|
223
|
+
lines.push('');
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// index.ts generator (configured client with typed table accessors)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
function generateIndex(schema) {
|
|
230
|
+
const tableEntries = Object.values(schema.tables);
|
|
231
|
+
const lines = [
|
|
232
|
+
...generatedFileHeader(),
|
|
233
|
+
"import { TurbineClient as BaseTurbineClient, QueryInterface } from 'turbine-orm';",
|
|
234
|
+
"import type { TurbineConfig } from 'turbine-orm';",
|
|
235
|
+
"import { SCHEMA } from './metadata.js';",
|
|
236
|
+
];
|
|
237
|
+
// Import all entity types and relations maps
|
|
238
|
+
const typeImports = [];
|
|
239
|
+
for (const t of tableEntries) {
|
|
240
|
+
typeImports.push(entityName(t.name));
|
|
241
|
+
if (Object.keys(t.relations).length > 0) {
|
|
242
|
+
typeImports.push(`${entityName(t.name)}Relations`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push(`import type { ${typeImports.join(', ')} } from './types.js';`);
|
|
246
|
+
lines.push('');
|
|
247
|
+
// Generate the client class with JSDoc
|
|
248
|
+
lines.push('/**');
|
|
249
|
+
lines.push(' * Generated Turbine client with typed table accessors.');
|
|
250
|
+
lines.push(' *');
|
|
251
|
+
lines.push(' * Tables:');
|
|
252
|
+
for (const table of tableEntries) {
|
|
253
|
+
lines.push(` * - \`${snakeToCamelStr(table.name)}\` (${table.name})`);
|
|
254
|
+
}
|
|
255
|
+
lines.push(' *');
|
|
256
|
+
lines.push(' * @example');
|
|
257
|
+
lines.push(' * ```ts');
|
|
258
|
+
lines.push(' * const db = turbine({ connectionString: process.env.DATABASE_URL });');
|
|
259
|
+
if (tableEntries.length > 0) {
|
|
260
|
+
const firstTable = tableEntries[0];
|
|
261
|
+
const accessor = snakeToCamelStr(firstTable.name);
|
|
262
|
+
lines.push(` * const rows = await db.${accessor}.findMany();`);
|
|
263
|
+
}
|
|
264
|
+
lines.push(' * ```');
|
|
265
|
+
lines.push(' */');
|
|
266
|
+
lines.push('export class TurbineClient extends BaseTurbineClient {');
|
|
267
|
+
for (const table of tableEntries) {
|
|
268
|
+
const typeName = entityName(table.name);
|
|
269
|
+
const accessor = snakeToCamelStr(table.name);
|
|
270
|
+
const hasRelations = Object.keys(table.relations).length > 0;
|
|
271
|
+
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
272
|
+
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
273
|
+
lines.push(` declare readonly ${accessor}: QueryInterface<${genericArgs}>;`);
|
|
274
|
+
}
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push(' constructor(config?: TurbineConfig) {');
|
|
277
|
+
lines.push(' super(config, SCHEMA);');
|
|
278
|
+
lines.push(' }');
|
|
279
|
+
lines.push('}');
|
|
280
|
+
lines.push('');
|
|
281
|
+
// Factory function with JSDoc
|
|
282
|
+
lines.push('/**');
|
|
283
|
+
lines.push(' * Create a new Turbine client instance.');
|
|
284
|
+
lines.push(' *');
|
|
285
|
+
lines.push(' * @param config - Connection configuration. Falls back to DATABASE_URL env var.');
|
|
286
|
+
lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
|
|
287
|
+
lines.push(' */');
|
|
288
|
+
lines.push('export function turbine(config?: TurbineConfig): TurbineClient {');
|
|
289
|
+
lines.push(' return new TurbineClient(config);');
|
|
290
|
+
lines.push('}');
|
|
291
|
+
lines.push('');
|
|
292
|
+
// Re-export everything
|
|
293
|
+
lines.push("export * from './types.js';");
|
|
294
|
+
lines.push("export { SCHEMA } from './metadata.js';");
|
|
295
|
+
lines.push('');
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Helpers
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
function serializeColumn(col) {
|
|
302
|
+
const parts = [
|
|
303
|
+
`name: '${escSQ(col.name)}'`,
|
|
304
|
+
`field: '${escSQ(col.field)}'`,
|
|
305
|
+
`pgType: '${escSQ(col.pgType)}'`,
|
|
306
|
+
`tsType: '${escSQ(col.tsType)}'`,
|
|
307
|
+
`nullable: ${col.nullable}`,
|
|
308
|
+
`hasDefault: ${col.hasDefault}`,
|
|
309
|
+
`isArray: ${col.isArray}`,
|
|
310
|
+
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
311
|
+
];
|
|
312
|
+
if (col.maxLength !== undefined)
|
|
313
|
+
parts.push(`maxLength: ${col.maxLength}`);
|
|
314
|
+
return `{ ${parts.join(', ')} }`;
|
|
315
|
+
}
|
|
316
|
+
function quoteIfNeeded(s) {
|
|
317
|
+
return /[^a-zA-Z0-9_$]/.test(s) ? `'${s}'` : s;
|
|
318
|
+
}
|
|
319
|
+
function snakeToCamelStr(s) {
|
|
320
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
321
|
+
}
|