@b9g/zen 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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +1044 -0
- package/chunk-2IEEEMRN.js +38 -0
- package/chunk-56M5Z3A6.js +1346 -0
- package/chunk-QXGEP5PB.js +310 -0
- package/ddl-NAJM37GQ.js +9 -0
- package/package.json +102 -0
- package/src/bun.d.ts +50 -0
- package/src/bun.js +906 -0
- package/src/mysql.d.ts +62 -0
- package/src/mysql.js +573 -0
- package/src/postgres.d.ts +62 -0
- package/src/postgres.js +555 -0
- package/src/sqlite.d.ts +43 -0
- package/src/sqlite.js +447 -0
- package/src/zen.d.ts +14 -0
- package/src/zen.js +2143 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
// src/impl/errors.ts
|
|
2
|
+
var DatabaseError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = "DatabaseError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
if (Error.captureStackTrace) {
|
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var ValidationError = class extends DatabaseError {
|
|
14
|
+
fieldErrors;
|
|
15
|
+
constructor(message, fieldErrors = {}, options) {
|
|
16
|
+
super("VALIDATION_ERROR", message, options);
|
|
17
|
+
this.name = "ValidationError";
|
|
18
|
+
this.fieldErrors = fieldErrors;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var TableDefinitionError = class extends DatabaseError {
|
|
22
|
+
tableName;
|
|
23
|
+
fieldName;
|
|
24
|
+
constructor(message, tableName, fieldName, options) {
|
|
25
|
+
super("TABLE_DEFINITION_ERROR", message, options);
|
|
26
|
+
this.name = "TableDefinitionError";
|
|
27
|
+
this.tableName = tableName;
|
|
28
|
+
this.fieldName = fieldName;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var MigrationError = class extends DatabaseError {
|
|
32
|
+
fromVersion;
|
|
33
|
+
toVersion;
|
|
34
|
+
constructor(message, fromVersion, toVersion, options) {
|
|
35
|
+
super("MIGRATION_ERROR", message, options);
|
|
36
|
+
this.name = "MigrationError";
|
|
37
|
+
this.fromVersion = fromVersion;
|
|
38
|
+
this.toVersion = toVersion;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var MigrationLockError = class extends DatabaseError {
|
|
42
|
+
constructor(message, options) {
|
|
43
|
+
super("MIGRATION_LOCK_ERROR", message, options);
|
|
44
|
+
this.name = "MigrationLockError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var QueryError = class extends DatabaseError {
|
|
48
|
+
sql;
|
|
49
|
+
constructor(message, sql, options) {
|
|
50
|
+
super("QUERY_ERROR", message, options);
|
|
51
|
+
this.name = "QueryError";
|
|
52
|
+
this.sql = sql;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var NotFoundError = class extends DatabaseError {
|
|
56
|
+
tableName;
|
|
57
|
+
id;
|
|
58
|
+
constructor(tableName, id, options) {
|
|
59
|
+
const message = id ? `${tableName} with id "${id}" not found` : `${tableName} not found`;
|
|
60
|
+
super("NOT_FOUND", message, options);
|
|
61
|
+
this.name = "NotFoundError";
|
|
62
|
+
this.tableName = tableName;
|
|
63
|
+
this.id = id;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var AlreadyExistsError = class extends DatabaseError {
|
|
67
|
+
tableName;
|
|
68
|
+
field;
|
|
69
|
+
value;
|
|
70
|
+
constructor(tableName, field, value, options) {
|
|
71
|
+
const message = field ? `${tableName} with ${field}="${value}" already exists` : `${tableName} already exists`;
|
|
72
|
+
super("ALREADY_EXISTS", message, options);
|
|
73
|
+
this.name = "AlreadyExistsError";
|
|
74
|
+
this.tableName = tableName;
|
|
75
|
+
this.field = field;
|
|
76
|
+
this.value = value;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var ConstraintViolationError = class extends DatabaseError {
|
|
80
|
+
/**
|
|
81
|
+
* Type of constraint that was violated.
|
|
82
|
+
* "unknown" if the specific type couldn't be determined from the error.
|
|
83
|
+
*/
|
|
84
|
+
kind;
|
|
85
|
+
/**
|
|
86
|
+
* Name of the constraint (e.g., "users_email_unique", "users.email").
|
|
87
|
+
* May be undefined if the database error didn't include it.
|
|
88
|
+
*/
|
|
89
|
+
constraint;
|
|
90
|
+
/**
|
|
91
|
+
* Table name where the violation occurred.
|
|
92
|
+
* May be undefined if not extractable from the error.
|
|
93
|
+
*/
|
|
94
|
+
table;
|
|
95
|
+
/**
|
|
96
|
+
* Column name involved in the violation.
|
|
97
|
+
* May be undefined if not extractable from the error.
|
|
98
|
+
*/
|
|
99
|
+
column;
|
|
100
|
+
constructor(message, details, options) {
|
|
101
|
+
super("CONSTRAINT_VIOLATION", message, options);
|
|
102
|
+
this.name = "ConstraintViolationError";
|
|
103
|
+
this.kind = details.kind;
|
|
104
|
+
this.constraint = details.constraint;
|
|
105
|
+
this.table = details.table;
|
|
106
|
+
this.column = details.column;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var ConnectionError = class extends DatabaseError {
|
|
110
|
+
constructor(message, options) {
|
|
111
|
+
super("CONNECTION_ERROR", message, options);
|
|
112
|
+
this.name = "ConnectionError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var TransactionError = class extends DatabaseError {
|
|
116
|
+
constructor(message, options) {
|
|
117
|
+
super("TRANSACTION_ERROR", message, options);
|
|
118
|
+
this.name = "TransactionError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var EnsureError = class extends DatabaseError {
|
|
122
|
+
/** The operation that failed */
|
|
123
|
+
operation;
|
|
124
|
+
/** The table being operated on */
|
|
125
|
+
table;
|
|
126
|
+
/** The step index where failure occurred (0-based) */
|
|
127
|
+
step;
|
|
128
|
+
constructor(message, details, options) {
|
|
129
|
+
super("ENSURE_ERROR", message, options);
|
|
130
|
+
this.name = "EnsureError";
|
|
131
|
+
this.operation = details.operation;
|
|
132
|
+
this.table = details.table;
|
|
133
|
+
this.step = details.step;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var SchemaDriftError = class extends DatabaseError {
|
|
137
|
+
/** The table where drift was detected */
|
|
138
|
+
table;
|
|
139
|
+
/** Description of what drifted */
|
|
140
|
+
drift;
|
|
141
|
+
/** Suggested action to resolve */
|
|
142
|
+
suggestion;
|
|
143
|
+
constructor(message, details, options) {
|
|
144
|
+
super("SCHEMA_DRIFT_ERROR", message, options);
|
|
145
|
+
this.name = "SchemaDriftError";
|
|
146
|
+
this.table = details.table;
|
|
147
|
+
this.drift = details.drift;
|
|
148
|
+
this.suggestion = details.suggestion;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var ConstraintPreflightError = class extends DatabaseError {
|
|
152
|
+
/** The table being constrained */
|
|
153
|
+
table;
|
|
154
|
+
/** The constraint being added (e.g., "unique:email" or "fk:authorId") */
|
|
155
|
+
constraint;
|
|
156
|
+
/** Number of violating rows */
|
|
157
|
+
violationCount;
|
|
158
|
+
/** The SQL query that found the violations - run it to see details */
|
|
159
|
+
query;
|
|
160
|
+
constructor(message, details, options) {
|
|
161
|
+
super("CONSTRAINT_PREFLIGHT_ERROR", message, options);
|
|
162
|
+
this.name = "ConstraintPreflightError";
|
|
163
|
+
this.table = details.table;
|
|
164
|
+
this.constraint = details.constraint;
|
|
165
|
+
this.violationCount = details.violationCount;
|
|
166
|
+
this.query = details.query;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function isDatabaseError(error) {
|
|
170
|
+
return error instanceof DatabaseError;
|
|
171
|
+
}
|
|
172
|
+
function hasErrorCode(error, code) {
|
|
173
|
+
return isDatabaseError(error) && error.code === code;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/impl/builtins.ts
|
|
177
|
+
var CURRENT_TIMESTAMP = Symbol.for("@b9g/zen:CURRENT_TIMESTAMP");
|
|
178
|
+
var CURRENT_DATE = Symbol.for("@b9g/zen:CURRENT_DATE");
|
|
179
|
+
var CURRENT_TIME = Symbol.for("@b9g/zen:CURRENT_TIME");
|
|
180
|
+
var NOW = CURRENT_TIMESTAMP;
|
|
181
|
+
var TODAY = CURRENT_DATE;
|
|
182
|
+
function isSQLBuiltin(value) {
|
|
183
|
+
if (typeof value !== "symbol")
|
|
184
|
+
return false;
|
|
185
|
+
const key = Symbol.keyFor(value);
|
|
186
|
+
return key === "@b9g/zen:CURRENT_TIMESTAMP" || key === "@b9g/zen:CURRENT_DATE" || key === "@b9g/zen:CURRENT_TIME";
|
|
187
|
+
}
|
|
188
|
+
function resolveSQLBuiltin(sym) {
|
|
189
|
+
const key = Symbol.keyFor(sym);
|
|
190
|
+
if (!key?.startsWith("@b9g/zen:")) {
|
|
191
|
+
throw new Error(`Unknown SQL builtin: ${String(sym)}`);
|
|
192
|
+
}
|
|
193
|
+
return key.slice("@b9g/zen:".length);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/impl/template.ts
|
|
197
|
+
var SQL_TEMPLATE = Symbol.for("@b9g/zen:template");
|
|
198
|
+
function createTemplate(strings, values = []) {
|
|
199
|
+
const tuple = [strings, ...values];
|
|
200
|
+
return Object.assign(tuple, { [SQL_TEMPLATE]: true });
|
|
201
|
+
}
|
|
202
|
+
function isSQLTemplate(value) {
|
|
203
|
+
return Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, SQL_TEMPLATE) && value[SQL_TEMPLATE] === true;
|
|
204
|
+
}
|
|
205
|
+
var SQL_IDENT = Symbol.for("@b9g/zen:ident");
|
|
206
|
+
function ident(name) {
|
|
207
|
+
return { [SQL_IDENT]: true, name };
|
|
208
|
+
}
|
|
209
|
+
function isSQLIdentifier(value) {
|
|
210
|
+
return value !== null && typeof value === "object" && SQL_IDENT in value && value[SQL_IDENT] === true;
|
|
211
|
+
}
|
|
212
|
+
function makeTemplate(parts) {
|
|
213
|
+
return Object.assign([...parts], { raw: parts });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/impl/table.ts
|
|
217
|
+
import { z } from "zod";
|
|
218
|
+
function validateIdentifier(name, type) {
|
|
219
|
+
const controlCharRegex = /[\x00-\x1f\x7f]/;
|
|
220
|
+
if (controlCharRegex.test(name)) {
|
|
221
|
+
throw new TableDefinitionError(
|
|
222
|
+
// eslint-disable-next-line no-control-regex
|
|
223
|
+
`Invalid ${type} identifier "${name.replace(/[\x00-\x1f\x7f]/g, "\\x")}": ${type} names cannot contain control characters`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (name.includes(";")) {
|
|
227
|
+
throw new TableDefinitionError(
|
|
228
|
+
`Invalid ${type} identifier "${name}": ${type} names cannot contain semicolons`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (name.includes("`")) {
|
|
232
|
+
throw new TableDefinitionError(
|
|
233
|
+
`Invalid ${type} identifier "${name}": ${type} names cannot contain backticks`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function validateWithStandardSchema(schema, data) {
|
|
238
|
+
const standard = schema["~standard"];
|
|
239
|
+
if (!standard?.validate) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"Schema does not implement Standard Schema (~standard.validate). Ensure you're using Zod v3.23+ or another Standard Schema-compliant library."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const result = standard.validate(data);
|
|
245
|
+
if (result && typeof result.then === "function") {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"Async validation is not supported. Standard Schema validate() must be synchronous."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (result.issues) {
|
|
251
|
+
throw new ValidationError(
|
|
252
|
+
"Validation failed",
|
|
253
|
+
result.issues.reduce(
|
|
254
|
+
(acc, issue) => {
|
|
255
|
+
const path = issue.path && issue.path.length > 0 ? issue.path.map(String).join(".") : "_root";
|
|
256
|
+
if (!acc[path])
|
|
257
|
+
acc[path] = [];
|
|
258
|
+
acc[path].push(issue.message);
|
|
259
|
+
return acc;
|
|
260
|
+
},
|
|
261
|
+
{}
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return result.value;
|
|
266
|
+
}
|
|
267
|
+
var DB_META_NAMESPACE = "db";
|
|
268
|
+
function getDBMeta(schema) {
|
|
269
|
+
try {
|
|
270
|
+
const meta = typeof schema.meta === "function" ? schema.meta() : {};
|
|
271
|
+
const dbMeta = meta?.[DB_META_NAMESPACE];
|
|
272
|
+
if (dbMeta && Object.keys(dbMeta).length > 0) {
|
|
273
|
+
return dbMeta;
|
|
274
|
+
}
|
|
275
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
|
|
276
|
+
return getDBMeta(schema.unwrap());
|
|
277
|
+
}
|
|
278
|
+
if (schema instanceof z.ZodDefault) {
|
|
279
|
+
return getDBMeta(schema.removeDefault());
|
|
280
|
+
}
|
|
281
|
+
if (schema instanceof z.ZodCatch) {
|
|
282
|
+
return getDBMeta(schema.removeCatch());
|
|
283
|
+
}
|
|
284
|
+
return {};
|
|
285
|
+
} catch {
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function setDBMeta(schema, dbMeta) {
|
|
290
|
+
const existing = (typeof schema.meta === "function" ? schema.meta() : void 0) ?? {};
|
|
291
|
+
return schema.meta({
|
|
292
|
+
...existing,
|
|
293
|
+
[DB_META_NAMESPACE]: {
|
|
294
|
+
...existing[DB_META_NAMESPACE] ?? {},
|
|
295
|
+
...dbMeta
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function isTemplateStringsArray(value) {
|
|
300
|
+
return Array.isArray(value) && "raw" in value;
|
|
301
|
+
}
|
|
302
|
+
function mergeFragment(strings, values, template, suffix) {
|
|
303
|
+
const templateStrings = template[0];
|
|
304
|
+
const templateValues = template.slice(1);
|
|
305
|
+
strings[strings.length - 1] += templateStrings[0];
|
|
306
|
+
for (let j = 1; j < templateStrings.length; j++) {
|
|
307
|
+
strings.push(templateStrings[j]);
|
|
308
|
+
}
|
|
309
|
+
values.push(...templateValues);
|
|
310
|
+
strings[strings.length - 1] += suffix;
|
|
311
|
+
}
|
|
312
|
+
function hasZodDefault(schema) {
|
|
313
|
+
return typeof schema.removeDefault === "function";
|
|
314
|
+
}
|
|
315
|
+
function isUuidSchema(schema) {
|
|
316
|
+
return schema instanceof z.ZodString && schema.format === "uuid";
|
|
317
|
+
}
|
|
318
|
+
function isIntSchema(schema) {
|
|
319
|
+
if (!(schema instanceof z.ZodNumber))
|
|
320
|
+
return false;
|
|
321
|
+
const checks = schema._def?.checks;
|
|
322
|
+
if (!Array.isArray(checks))
|
|
323
|
+
return false;
|
|
324
|
+
return checks.some((c) => c.isInt === true);
|
|
325
|
+
}
|
|
326
|
+
function createDBMethods(schema) {
|
|
327
|
+
return {
|
|
328
|
+
/**
|
|
329
|
+
* Mark field as primary key.
|
|
330
|
+
* @example z.string().uuid().db.primary()
|
|
331
|
+
*/
|
|
332
|
+
primary() {
|
|
333
|
+
return setDBMeta(schema, { primary: true });
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Mark field as unique.
|
|
337
|
+
* @example z.string().email().db.unique()
|
|
338
|
+
*/
|
|
339
|
+
unique() {
|
|
340
|
+
return setDBMeta(schema, { unique: true });
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* Create an index on this field.
|
|
344
|
+
* @example z.date().db.index()
|
|
345
|
+
*/
|
|
346
|
+
index() {
|
|
347
|
+
return setDBMeta(schema, { indexed: true });
|
|
348
|
+
},
|
|
349
|
+
/**
|
|
350
|
+
* Mark field as soft delete timestamp.
|
|
351
|
+
* @example z.date().nullable().default(null).db.softDelete()
|
|
352
|
+
*/
|
|
353
|
+
softDelete() {
|
|
354
|
+
return setDBMeta(schema, { softDelete: true });
|
|
355
|
+
},
|
|
356
|
+
/**
|
|
357
|
+
* Define a foreign key reference with optional reverse relationship.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* // Forward reference only
|
|
361
|
+
* authorId: z.string().uuid().db.references(Users, "author")
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* // With options
|
|
365
|
+
* authorId: z.string().uuid().db.references(Users, "author", {
|
|
366
|
+
* reverseAs: "posts", // user.posts = Post[]
|
|
367
|
+
* ondelete: "cascade",
|
|
368
|
+
* })
|
|
369
|
+
*/
|
|
370
|
+
references(table2, as, options) {
|
|
371
|
+
return setDBMeta(schema, {
|
|
372
|
+
reference: {
|
|
373
|
+
table: table2,
|
|
374
|
+
as,
|
|
375
|
+
...options
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
/**
|
|
380
|
+
* Encode app values to DB values (for INSERT/UPDATE).
|
|
381
|
+
* One-way transformation is fine (e.g., password hashing).
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* password: z.string().db.encode(hashPassword)
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* // Bidirectional: pair with .db.decode()
|
|
388
|
+
* status: z.enum(["pending", "active"])
|
|
389
|
+
* .db.encode(s => statusMap.indexOf(s))
|
|
390
|
+
* .db.decode(i => statusMap[i])
|
|
391
|
+
*/
|
|
392
|
+
encode(encodeFn) {
|
|
393
|
+
const existing = getDBMeta(schema);
|
|
394
|
+
if (existing.inserted || existing.updated) {
|
|
395
|
+
throw new TableDefinitionError(
|
|
396
|
+
`encode() cannot be combined with inserted() or updated(). DB expressions bypass encoding and are sent directly to the database.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return setDBMeta(schema, { encode: encodeFn });
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* Decode DB values to app values (for SELECT).
|
|
403
|
+
* One-way transformation is fine.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* legacy: z.string().db.decode(deserializeLegacyFormat)
|
|
407
|
+
*/
|
|
408
|
+
decode(decodeFn) {
|
|
409
|
+
const existing = getDBMeta(schema);
|
|
410
|
+
if (existing.inserted || existing.updated) {
|
|
411
|
+
throw new TableDefinitionError(
|
|
412
|
+
`decode() cannot be combined with inserted() or updated(). DB expressions bypass decoding and are sent directly to the database.`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return setDBMeta(schema, { decode: decodeFn });
|
|
416
|
+
},
|
|
417
|
+
/**
|
|
418
|
+
* Specify explicit column type for DDL generation.
|
|
419
|
+
* Required when using custom encode/decode on objects/arrays
|
|
420
|
+
* that transform to a different storage type.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* // Store array as CSV instead of JSON
|
|
424
|
+
* tags: z.array(z.string())
|
|
425
|
+
* .db.encode((arr) => arr.join(","))
|
|
426
|
+
* .db.decode((str) => str.split(","))
|
|
427
|
+
* .db.type("TEXT")
|
|
428
|
+
*/
|
|
429
|
+
type(columnType) {
|
|
430
|
+
return setDBMeta(schema, { columnType });
|
|
431
|
+
},
|
|
432
|
+
/**
|
|
433
|
+
* Set a value to apply on INSERT.
|
|
434
|
+
*
|
|
435
|
+
* Three forms:
|
|
436
|
+
* - Tagged template: .db.inserted`CURRENT_TIMESTAMP` → raw SQL
|
|
437
|
+
* - Symbol: .db.inserted(NOW) → dialect-aware SQL
|
|
438
|
+
* - Function: .db.inserted(() => "draft") → client-side per-insert
|
|
439
|
+
*
|
|
440
|
+
* Field becomes optional for insert.
|
|
441
|
+
*
|
|
442
|
+
* **Note:** SQL expressions (tagged templates and symbols) bypass encode/decode
|
|
443
|
+
* since they're executed by the database, not the application. Use function
|
|
444
|
+
* form if you need encoding applied.
|
|
445
|
+
*
|
|
446
|
+
* **Note:** Interpolated values in tagged templates are parameterized but not
|
|
447
|
+
* schema-validated. Ensure values are appropriate for the column type.
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* createdAt: z.date().db.inserted(NOW)
|
|
451
|
+
* token: z.string().db.inserted(() => crypto.randomUUID())
|
|
452
|
+
* slug: z.string().db.inserted`LOWER(name)`
|
|
453
|
+
*/
|
|
454
|
+
inserted(stringsOrValue, ...templateValues) {
|
|
455
|
+
let insertedMeta;
|
|
456
|
+
if (isTemplateStringsArray(stringsOrValue)) {
|
|
457
|
+
const strings = [];
|
|
458
|
+
const values = [];
|
|
459
|
+
for (let i = 0; i < stringsOrValue.length; i++) {
|
|
460
|
+
if (i === 0) {
|
|
461
|
+
strings.push(stringsOrValue[i]);
|
|
462
|
+
}
|
|
463
|
+
if (i < templateValues.length) {
|
|
464
|
+
const value = templateValues[i];
|
|
465
|
+
if (isSQLTemplate(value)) {
|
|
466
|
+
mergeFragment(strings, values, value, stringsOrValue[i + 1]);
|
|
467
|
+
} else {
|
|
468
|
+
values.push(value);
|
|
469
|
+
strings.push(stringsOrValue[i + 1]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
insertedMeta = {
|
|
474
|
+
type: "sql",
|
|
475
|
+
template: createTemplate(makeTemplate(strings), values)
|
|
476
|
+
};
|
|
477
|
+
} else if (isSQLBuiltin(stringsOrValue)) {
|
|
478
|
+
insertedMeta = { type: "symbol", symbol: stringsOrValue };
|
|
479
|
+
} else if (typeof stringsOrValue === "function") {
|
|
480
|
+
insertedMeta = { type: "function", fn: stringsOrValue };
|
|
481
|
+
} else {
|
|
482
|
+
throw new TableDefinitionError(
|
|
483
|
+
`inserted() requires a tagged template, symbol (NOW), or function. Got: ${typeof stringsOrValue}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const existing = getDBMeta(schema);
|
|
487
|
+
if (existing.encode || existing.decode) {
|
|
488
|
+
throw new TableDefinitionError(
|
|
489
|
+
`inserted() cannot be combined with encode() or decode(). DB expressions and functions bypass encoding/decoding.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const optionalSchema = schema.optional();
|
|
493
|
+
return setDBMeta(optionalSchema, { ...existing, inserted: insertedMeta });
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Set a value to apply on UPDATE only.
|
|
497
|
+
*
|
|
498
|
+
* Same forms as inserted(). See inserted() for notes on codec bypass
|
|
499
|
+
* and template parameter validation.
|
|
500
|
+
*
|
|
501
|
+
* Field becomes optional for update operations.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* modifiedAt: z.date().db.updated(NOW)
|
|
505
|
+
* lastModified: z.date().db.updated(() => new Date())
|
|
506
|
+
*/
|
|
507
|
+
updated(stringsOrValue, ...templateValues) {
|
|
508
|
+
let updatedMeta;
|
|
509
|
+
if (isTemplateStringsArray(stringsOrValue)) {
|
|
510
|
+
const strings = [];
|
|
511
|
+
const values = [];
|
|
512
|
+
for (let i = 0; i < stringsOrValue.length; i++) {
|
|
513
|
+
if (i === 0) {
|
|
514
|
+
strings.push(stringsOrValue[i]);
|
|
515
|
+
}
|
|
516
|
+
if (i < templateValues.length) {
|
|
517
|
+
const value = templateValues[i];
|
|
518
|
+
if (isSQLTemplate(value)) {
|
|
519
|
+
mergeFragment(strings, values, value, stringsOrValue[i + 1]);
|
|
520
|
+
} else {
|
|
521
|
+
values.push(value);
|
|
522
|
+
strings.push(stringsOrValue[i + 1]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
updatedMeta = {
|
|
527
|
+
type: "sql",
|
|
528
|
+
template: createTemplate(makeTemplate(strings), values)
|
|
529
|
+
};
|
|
530
|
+
} else if (isSQLBuiltin(stringsOrValue)) {
|
|
531
|
+
updatedMeta = { type: "symbol", symbol: stringsOrValue };
|
|
532
|
+
} else if (typeof stringsOrValue === "function") {
|
|
533
|
+
updatedMeta = { type: "function", fn: stringsOrValue };
|
|
534
|
+
} else {
|
|
535
|
+
throw new TableDefinitionError(
|
|
536
|
+
`updated() requires a tagged template, symbol (NOW), or function. Got: ${typeof stringsOrValue}`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const existing = getDBMeta(schema);
|
|
540
|
+
if (existing.encode || existing.decode) {
|
|
541
|
+
throw new TableDefinitionError(
|
|
542
|
+
`updated() cannot be combined with encode() or decode(). DB expressions and functions bypass encoding/decoding.`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const optionalSchema = schema.optional();
|
|
546
|
+
return setDBMeta(optionalSchema, { ...existing, updated: updatedMeta });
|
|
547
|
+
},
|
|
548
|
+
/**
|
|
549
|
+
* Set a value to apply on both INSERT and UPDATE.
|
|
550
|
+
*
|
|
551
|
+
* Same forms as inserted(). See inserted() for notes on codec bypass
|
|
552
|
+
* and template parameter validation.
|
|
553
|
+
*
|
|
554
|
+
* Field becomes optional for insert/update.
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* updatedAt: z.date().db.upserted(NOW)
|
|
558
|
+
* lastModified: z.date().db.upserted(() => new Date())
|
|
559
|
+
*/
|
|
560
|
+
upserted(stringsOrValue, ...templateValues) {
|
|
561
|
+
let upsertedMeta;
|
|
562
|
+
if (isTemplateStringsArray(stringsOrValue)) {
|
|
563
|
+
const strings = [];
|
|
564
|
+
const values = [];
|
|
565
|
+
for (let i = 0; i < stringsOrValue.length; i++) {
|
|
566
|
+
if (i === 0) {
|
|
567
|
+
strings.push(stringsOrValue[i]);
|
|
568
|
+
}
|
|
569
|
+
if (i < templateValues.length) {
|
|
570
|
+
const value = templateValues[i];
|
|
571
|
+
if (isSQLTemplate(value)) {
|
|
572
|
+
mergeFragment(strings, values, value, stringsOrValue[i + 1]);
|
|
573
|
+
} else {
|
|
574
|
+
values.push(value);
|
|
575
|
+
strings.push(stringsOrValue[i + 1]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
upsertedMeta = {
|
|
580
|
+
type: "sql",
|
|
581
|
+
template: createTemplate(makeTemplate(strings), values)
|
|
582
|
+
};
|
|
583
|
+
} else if (isSQLBuiltin(stringsOrValue)) {
|
|
584
|
+
upsertedMeta = { type: "symbol", symbol: stringsOrValue };
|
|
585
|
+
} else if (typeof stringsOrValue === "function") {
|
|
586
|
+
upsertedMeta = { type: "function", fn: stringsOrValue };
|
|
587
|
+
} else {
|
|
588
|
+
throw new TableDefinitionError(
|
|
589
|
+
`upserted() requires a tagged template, symbol (NOW), or function. Got: ${typeof stringsOrValue}`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const existing = getDBMeta(schema);
|
|
593
|
+
if (existing.encode || existing.decode) {
|
|
594
|
+
throw new TableDefinitionError(
|
|
595
|
+
`upserted() cannot be combined with encode() or decode(). DB expressions and functions bypass encoding/decoding.`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const optionalSchema = schema.optional();
|
|
599
|
+
return setDBMeta(optionalSchema, { ...existing, upserted: upsertedMeta });
|
|
600
|
+
},
|
|
601
|
+
/**
|
|
602
|
+
* Auto-generate value on insert based on field type.
|
|
603
|
+
*
|
|
604
|
+
* Type-aware behavior:
|
|
605
|
+
* - `z.string().uuid()` → generates UUID via `crypto.randomUUID()`
|
|
606
|
+
* - `z.number().int()` on primary key → auto-increment (database-side)
|
|
607
|
+
* - `z.date()` → current timestamp via NOW
|
|
608
|
+
*
|
|
609
|
+
* Field becomes optional for insert.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* id: z.string().uuid().db.primary().db.auto()
|
|
613
|
+
* // → crypto.randomUUID() on insert
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* id: z.number().int().db.primary().db.auto()
|
|
617
|
+
* // → auto-increment
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* createdAt: z.date().db.auto()
|
|
621
|
+
* // → NOW on insert
|
|
622
|
+
*/
|
|
623
|
+
auto() {
|
|
624
|
+
const existing = getDBMeta(schema);
|
|
625
|
+
const optionalSchema = schema.optional();
|
|
626
|
+
if (isUuidSchema(schema)) {
|
|
627
|
+
const insertedMeta = {
|
|
628
|
+
type: "function",
|
|
629
|
+
fn: () => crypto.randomUUID()
|
|
630
|
+
};
|
|
631
|
+
return setDBMeta(optionalSchema, { ...existing, inserted: insertedMeta });
|
|
632
|
+
}
|
|
633
|
+
if (isIntSchema(schema)) {
|
|
634
|
+
return setDBMeta(optionalSchema, { ...existing, autoIncrement: true });
|
|
635
|
+
}
|
|
636
|
+
if (schema instanceof z.ZodDate) {
|
|
637
|
+
const insertedMeta = {
|
|
638
|
+
type: "symbol",
|
|
639
|
+
symbol: NOW
|
|
640
|
+
};
|
|
641
|
+
return setDBMeta(optionalSchema, { ...existing, inserted: insertedMeta });
|
|
642
|
+
}
|
|
643
|
+
throw new Error(
|
|
644
|
+
`.db.auto() is not supported for this type. Supported: z.string().uuid(), z.number().int(), z.date()`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function extendZod(zodModule) {
|
|
650
|
+
for (const key of Object.keys(zodModule)) {
|
|
651
|
+
const value = zodModule[key];
|
|
652
|
+
if (typeof value === "function" && value.prototype && key.startsWith("Zod")) {
|
|
653
|
+
if (!("db" in value.prototype)) {
|
|
654
|
+
Object.defineProperty(value.prototype, "db", {
|
|
655
|
+
get() {
|
|
656
|
+
return createDBMethods(this);
|
|
657
|
+
},
|
|
658
|
+
enumerable: false,
|
|
659
|
+
configurable: true
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
extendZod(z);
|
|
666
|
+
var TABLE_MARKER = Symbol.for("@b9g/zen:table");
|
|
667
|
+
var TABLE_META = Symbol.for("@b9g/zen:table-meta");
|
|
668
|
+
function getTableMeta(table2) {
|
|
669
|
+
return table2[TABLE_META];
|
|
670
|
+
}
|
|
671
|
+
function table(name, shape, options = {}) {
|
|
672
|
+
validateIdentifier(name, "table");
|
|
673
|
+
if (name.includes(".")) {
|
|
674
|
+
throw new TableDefinitionError(
|
|
675
|
+
`Invalid table name "${name}": table names cannot contain "." as it conflicts with normalization prefixes`,
|
|
676
|
+
name
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
const zodShape = {};
|
|
680
|
+
const meta = {
|
|
681
|
+
primary: null,
|
|
682
|
+
unique: [],
|
|
683
|
+
indexed: [],
|
|
684
|
+
softDeleteField: null,
|
|
685
|
+
references: [],
|
|
686
|
+
fields: {}
|
|
687
|
+
};
|
|
688
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
689
|
+
validateIdentifier(key, "column");
|
|
690
|
+
if (key.includes(".")) {
|
|
691
|
+
throw new TableDefinitionError(
|
|
692
|
+
`Invalid field name "${key}" in table "${name}": field names cannot contain "." as it conflicts with normalization prefixes`,
|
|
693
|
+
name,
|
|
694
|
+
key
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const fieldSchema = value;
|
|
698
|
+
zodShape[key] = fieldSchema;
|
|
699
|
+
if (hasZodDefault(fieldSchema)) {
|
|
700
|
+
throw new TableDefinitionError(
|
|
701
|
+
`Field "${key}" uses Zod .default() which applies at parse time, not write time. Use .db.inserted() or .db.updated() instead.`,
|
|
702
|
+
name,
|
|
703
|
+
key
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const fieldDBMeta = getDBMeta(fieldSchema);
|
|
707
|
+
const dbMeta = {};
|
|
708
|
+
if (fieldDBMeta.primary) {
|
|
709
|
+
if (meta.primary !== null) {
|
|
710
|
+
throw new TableDefinitionError(
|
|
711
|
+
`Table "${name}" has multiple primary keys: "${meta.primary}" and "${key}". Only one primary key is allowed.`,
|
|
712
|
+
name
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
meta.primary = key;
|
|
716
|
+
dbMeta.primaryKey = true;
|
|
717
|
+
}
|
|
718
|
+
if (fieldDBMeta.unique) {
|
|
719
|
+
meta.unique.push(key);
|
|
720
|
+
dbMeta.unique = true;
|
|
721
|
+
}
|
|
722
|
+
if (fieldDBMeta.indexed) {
|
|
723
|
+
meta.indexed.push(key);
|
|
724
|
+
dbMeta.indexed = true;
|
|
725
|
+
}
|
|
726
|
+
if (fieldDBMeta.softDelete) {
|
|
727
|
+
if (meta.softDeleteField !== null) {
|
|
728
|
+
throw new TableDefinitionError(
|
|
729
|
+
`Table "${name}" has multiple soft delete fields: "${meta.softDeleteField}" and "${key}". Only one soft delete field is allowed.`,
|
|
730
|
+
name
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
meta.softDeleteField = key;
|
|
734
|
+
dbMeta.softDelete = true;
|
|
735
|
+
}
|
|
736
|
+
if (fieldDBMeta.reference) {
|
|
737
|
+
const ref = fieldDBMeta.reference;
|
|
738
|
+
if (ref.as in shape) {
|
|
739
|
+
throw new TableDefinitionError(
|
|
740
|
+
`Table "${name}": reference property "${ref.as}" (from field "${key}") collides with existing schema field. Choose a different 'as' name.`,
|
|
741
|
+
name,
|
|
742
|
+
key
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (options.derive && ref.as in options.derive) {
|
|
746
|
+
throw new TableDefinitionError(
|
|
747
|
+
`Table "${name}": reference property "${ref.as}" (from field "${key}") collides with derived property. Choose a different 'as' name.`,
|
|
748
|
+
name,
|
|
749
|
+
key
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const existingRef = meta.references.find((r) => r.as === ref.as);
|
|
753
|
+
if (existingRef) {
|
|
754
|
+
throw new TableDefinitionError(
|
|
755
|
+
`Table "${name}": duplicate reference alias "${ref.as}" used by fields "${existingRef.fieldName}" and "${key}". Each reference must have a unique 'as' name.`,
|
|
756
|
+
name,
|
|
757
|
+
key
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (ref.reverseAs) {
|
|
761
|
+
const targetShape = ref.table.schema.shape;
|
|
762
|
+
if (ref.reverseAs in targetShape) {
|
|
763
|
+
throw new TableDefinitionError(
|
|
764
|
+
`Table "${name}": reverse reference property "${ref.reverseAs}" (from field "${key}") collides with existing field in target table "${ref.table.name}". Choose a different 'reverseAs' name.`,
|
|
765
|
+
name,
|
|
766
|
+
key
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
meta.references.push({
|
|
771
|
+
fieldName: key,
|
|
772
|
+
table: ref.table,
|
|
773
|
+
referencedField: ref.field ?? getTableMeta(ref.table).primary ?? "id",
|
|
774
|
+
as: ref.as,
|
|
775
|
+
reverseAs: ref.reverseAs,
|
|
776
|
+
onDelete: ref.onDelete
|
|
777
|
+
});
|
|
778
|
+
dbMeta.reference = ref;
|
|
779
|
+
}
|
|
780
|
+
if (fieldDBMeta.encode) {
|
|
781
|
+
dbMeta.encode = fieldDBMeta.encode;
|
|
782
|
+
}
|
|
783
|
+
if (fieldDBMeta.decode) {
|
|
784
|
+
dbMeta.decode = fieldDBMeta.decode;
|
|
785
|
+
}
|
|
786
|
+
if (fieldDBMeta.columnType) {
|
|
787
|
+
dbMeta.columnType = fieldDBMeta.columnType;
|
|
788
|
+
}
|
|
789
|
+
if (fieldDBMeta.inserted) {
|
|
790
|
+
dbMeta.inserted = fieldDBMeta.inserted;
|
|
791
|
+
}
|
|
792
|
+
if (fieldDBMeta.updated) {
|
|
793
|
+
dbMeta.updated = fieldDBMeta.updated;
|
|
794
|
+
}
|
|
795
|
+
if (fieldDBMeta.upserted) {
|
|
796
|
+
dbMeta.upserted = fieldDBMeta.upserted;
|
|
797
|
+
}
|
|
798
|
+
if (fieldDBMeta.autoIncrement) {
|
|
799
|
+
dbMeta.autoIncrement = fieldDBMeta.autoIncrement;
|
|
800
|
+
}
|
|
801
|
+
meta.fields[key] = dbMeta;
|
|
802
|
+
}
|
|
803
|
+
const schema = z.object(zodShape);
|
|
804
|
+
return createTableObject(name, schema, zodShape, meta, options);
|
|
805
|
+
}
|
|
806
|
+
function createColsProxy(tableName, zodShape) {
|
|
807
|
+
return new Proxy({}, {
|
|
808
|
+
get(_target, prop) {
|
|
809
|
+
if (prop in zodShape) {
|
|
810
|
+
return createTemplate(makeTemplate(["", ".", ""]), [
|
|
811
|
+
ident(tableName),
|
|
812
|
+
ident(prop)
|
|
813
|
+
]);
|
|
814
|
+
}
|
|
815
|
+
return void 0;
|
|
816
|
+
},
|
|
817
|
+
has(_target, prop) {
|
|
818
|
+
return prop in zodShape;
|
|
819
|
+
},
|
|
820
|
+
ownKeys() {
|
|
821
|
+
return Object.keys(zodShape);
|
|
822
|
+
},
|
|
823
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
824
|
+
if (prop in zodShape) {
|
|
825
|
+
return { enumerable: true, configurable: true };
|
|
826
|
+
}
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function createTableObject(name, schema, zodShape, meta, options) {
|
|
832
|
+
const cols = createColsProxy(name, zodShape);
|
|
833
|
+
const primary = meta.primary ? createTemplate(makeTemplate(["", ".", ""]), [
|
|
834
|
+
ident(name),
|
|
835
|
+
ident(meta.primary)
|
|
836
|
+
]) : null;
|
|
837
|
+
if (options.derive) {
|
|
838
|
+
for (const key of Object.keys(options.derive)) {
|
|
839
|
+
if (key in zodShape) {
|
|
840
|
+
throw new TableDefinitionError(
|
|
841
|
+
`Table "${name}": derived property "${key}" collides with existing schema field. Choose a different name.`,
|
|
842
|
+
name
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const internalMeta = { ...meta, derive: options.derive };
|
|
848
|
+
const table2 = {
|
|
849
|
+
[TABLE_MARKER]: true,
|
|
850
|
+
[TABLE_META]: internalMeta,
|
|
851
|
+
name,
|
|
852
|
+
schema,
|
|
853
|
+
indexes: options.indexes ?? [],
|
|
854
|
+
unique: options.unique ?? [],
|
|
855
|
+
compoundReferences: options.references ?? [],
|
|
856
|
+
cols,
|
|
857
|
+
primary,
|
|
858
|
+
// Internal getter for backward compatibility - use getTableMeta() instead
|
|
859
|
+
get meta() {
|
|
860
|
+
return internalMeta;
|
|
861
|
+
},
|
|
862
|
+
fields() {
|
|
863
|
+
const result = {};
|
|
864
|
+
for (const [key, zodType] of Object.entries(zodShape)) {
|
|
865
|
+
const dbMeta = meta.fields[key] || {};
|
|
866
|
+
result[key] = extractFieldMeta(key, zodType, dbMeta);
|
|
867
|
+
}
|
|
868
|
+
for (const ref of meta.references) {
|
|
869
|
+
result[ref.as] = {
|
|
870
|
+
fields: () => ref.table.fields(),
|
|
871
|
+
table: ref.table
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
return result;
|
|
875
|
+
},
|
|
876
|
+
primaryKey() {
|
|
877
|
+
return meta.primary;
|
|
878
|
+
},
|
|
879
|
+
references() {
|
|
880
|
+
return meta.references;
|
|
881
|
+
},
|
|
882
|
+
deleted() {
|
|
883
|
+
const softDeleteField = meta.softDeleteField;
|
|
884
|
+
if (!softDeleteField) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
`Table "${name}" does not have a soft delete field. Use softDelete() wrapper to mark a field.`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
return createTemplate(makeTemplate(["", ".", " IS NOT NULL"]), [
|
|
890
|
+
ident(name),
|
|
891
|
+
ident(softDeleteField)
|
|
892
|
+
]);
|
|
893
|
+
},
|
|
894
|
+
in(field, values) {
|
|
895
|
+
if (!(field in zodShape)) {
|
|
896
|
+
throw new Error(
|
|
897
|
+
`Field "${field}" does not exist in table "${name}". Available fields: ${Object.keys(zodShape).join(", ")}`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
if (values.length === 0) {
|
|
901
|
+
return createTemplate(makeTemplate(["1 = 0"]), []);
|
|
902
|
+
}
|
|
903
|
+
const POSTGRESQL_PARAM_LIMIT = 32767;
|
|
904
|
+
if (values.length > POSTGRESQL_PARAM_LIMIT) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`Too many values in IN clause: ${values.length} exceeds PostgreSQL's parameter limit of ${POSTGRESQL_PARAM_LIMIT}. Consider using a temporary table or splitting the query.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const strings = ["", ".", " IN ("];
|
|
910
|
+
const templateValues = [ident(name), ident(field)];
|
|
911
|
+
for (let i = 0; i < values.length; i++) {
|
|
912
|
+
templateValues.push(values[i]);
|
|
913
|
+
strings.push(i < values.length - 1 ? ", " : ")");
|
|
914
|
+
}
|
|
915
|
+
return createTemplate(makeTemplate(strings), templateValues);
|
|
916
|
+
},
|
|
917
|
+
pick(...fields) {
|
|
918
|
+
const fieldSet = new Set(fields);
|
|
919
|
+
const pickObj = {};
|
|
920
|
+
for (const f of fields) {
|
|
921
|
+
pickObj[f] = true;
|
|
922
|
+
}
|
|
923
|
+
const pickedSchema = schema.pick(pickObj);
|
|
924
|
+
const pickedZodShape = {};
|
|
925
|
+
for (const f of fields) {
|
|
926
|
+
if (f in zodShape) {
|
|
927
|
+
pickedZodShape[f] = zodShape[f];
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const existingDerivedExprs = meta.derivedExprs ?? [];
|
|
931
|
+
const existingDerivedFields = meta.derivedFields ?? [];
|
|
932
|
+
const pickedDerivedExprs = existingDerivedExprs.filter(
|
|
933
|
+
(expr) => fieldSet.has(expr.fieldName)
|
|
934
|
+
);
|
|
935
|
+
const pickedDerivedFields = existingDerivedFields.filter(
|
|
936
|
+
(f) => fieldSet.has(f)
|
|
937
|
+
);
|
|
938
|
+
const pickedMeta = {
|
|
939
|
+
primary: meta.primary && fieldSet.has(meta.primary) ? meta.primary : null,
|
|
940
|
+
unique: meta.unique.filter((f) => fieldSet.has(f)),
|
|
941
|
+
indexed: meta.indexed.filter((f) => fieldSet.has(f)),
|
|
942
|
+
softDeleteField: meta.softDeleteField && fieldSet.has(meta.softDeleteField) ? meta.softDeleteField : null,
|
|
943
|
+
references: meta.references.filter((r) => fieldSet.has(r.fieldName)),
|
|
944
|
+
fields: Object.fromEntries(
|
|
945
|
+
Object.entries(meta.fields).filter(([k]) => fieldSet.has(k))
|
|
946
|
+
),
|
|
947
|
+
isPartial: true,
|
|
948
|
+
isDerived: void 0,
|
|
949
|
+
derivedExprs: void 0,
|
|
950
|
+
derivedFields: void 0
|
|
951
|
+
};
|
|
952
|
+
if (pickedDerivedExprs.length > 0) {
|
|
953
|
+
pickedMeta.isDerived = true;
|
|
954
|
+
pickedMeta.derivedExprs = pickedDerivedExprs;
|
|
955
|
+
pickedMeta.derivedFields = pickedDerivedFields;
|
|
956
|
+
}
|
|
957
|
+
const pickedIndexes = (options.indexes ?? []).filter(
|
|
958
|
+
(idx) => idx.every((f) => fieldSet.has(f))
|
|
959
|
+
);
|
|
960
|
+
const pickedUnique = (options.unique ?? []).filter(
|
|
961
|
+
(u) => u.every((f) => fieldSet.has(f))
|
|
962
|
+
);
|
|
963
|
+
const pickedCompoundRefs = (options.references ?? []).filter(
|
|
964
|
+
(ref) => ref.fields.every((f) => fieldSet.has(f))
|
|
965
|
+
);
|
|
966
|
+
return createTableObject(name, pickedSchema, pickedZodShape, pickedMeta, {
|
|
967
|
+
indexes: pickedIndexes,
|
|
968
|
+
unique: pickedUnique,
|
|
969
|
+
references: pickedCompoundRefs
|
|
970
|
+
});
|
|
971
|
+
},
|
|
972
|
+
derive(fieldName, fieldType) {
|
|
973
|
+
return (stringsOrValue, ...templateValues) => {
|
|
974
|
+
const strings = [];
|
|
975
|
+
const values = [];
|
|
976
|
+
for (let i = 0; i < stringsOrValue.length; i++) {
|
|
977
|
+
if (i === 0) {
|
|
978
|
+
strings.push(stringsOrValue[i]);
|
|
979
|
+
}
|
|
980
|
+
if (i < templateValues.length) {
|
|
981
|
+
const value = templateValues[i];
|
|
982
|
+
if (isSQLTemplate(value)) {
|
|
983
|
+
mergeFragment(strings, values, value, stringsOrValue[i + 1]);
|
|
984
|
+
} else {
|
|
985
|
+
values.push(value);
|
|
986
|
+
strings.push(stringsOrValue[i + 1]);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (strings.length > 0) {
|
|
991
|
+
strings[0] = strings[0].trimStart();
|
|
992
|
+
strings[strings.length - 1] = strings[strings.length - 1].trimEnd();
|
|
993
|
+
}
|
|
994
|
+
const derivedExpr = {
|
|
995
|
+
fieldName,
|
|
996
|
+
template: createTemplate(makeTemplate(strings), values)
|
|
997
|
+
};
|
|
998
|
+
const mergedSchema = schema.extend({ [fieldName]: fieldType });
|
|
999
|
+
const mergedZodShape = { ...zodShape, [fieldName]: fieldType };
|
|
1000
|
+
const existingExprs = meta.derivedExprs ?? [];
|
|
1001
|
+
const existingDerivedFields = meta.derivedFields ?? [];
|
|
1002
|
+
const derivedMeta = {
|
|
1003
|
+
...meta,
|
|
1004
|
+
isDerived: true,
|
|
1005
|
+
derivedExprs: [...existingExprs, derivedExpr],
|
|
1006
|
+
derivedFields: [...existingDerivedFields, fieldName],
|
|
1007
|
+
fields: {
|
|
1008
|
+
...meta.fields,
|
|
1009
|
+
[fieldName]: {}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
return createTableObject(
|
|
1013
|
+
name,
|
|
1014
|
+
mergedSchema,
|
|
1015
|
+
mergedZodShape,
|
|
1016
|
+
derivedMeta,
|
|
1017
|
+
options
|
|
1018
|
+
);
|
|
1019
|
+
};
|
|
1020
|
+
},
|
|
1021
|
+
set(values) {
|
|
1022
|
+
const entries = Object.entries(values).filter(([, v]) => v !== void 0);
|
|
1023
|
+
if (entries.length === 0) {
|
|
1024
|
+
throw new Error("set() requires at least one non-undefined field");
|
|
1025
|
+
}
|
|
1026
|
+
const strings = [""];
|
|
1027
|
+
const templateValues = [];
|
|
1028
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1029
|
+
const [field, value] = entries[i];
|
|
1030
|
+
templateValues.push(ident(field));
|
|
1031
|
+
strings.push(" = ");
|
|
1032
|
+
templateValues.push(value);
|
|
1033
|
+
strings.push(i < entries.length - 1 ? ", " : "");
|
|
1034
|
+
}
|
|
1035
|
+
return createTemplate(makeTemplate(strings), templateValues);
|
|
1036
|
+
},
|
|
1037
|
+
on(referencingTable, alias) {
|
|
1038
|
+
const refs = getTableMeta(referencingTable).references.filter(
|
|
1039
|
+
(r) => r.table.name === name
|
|
1040
|
+
);
|
|
1041
|
+
if (refs.length === 0) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Table "${referencingTable.name}" has no foreign key references to "${name}"`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
let ref;
|
|
1047
|
+
if (refs.length === 1) {
|
|
1048
|
+
ref = refs[0];
|
|
1049
|
+
} else if (alias) {
|
|
1050
|
+
const found = refs.find((r) => r.as === alias);
|
|
1051
|
+
if (!found) {
|
|
1052
|
+
const availableAliases = refs.map((r) => `"${r.as}"`).join(", ");
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`No foreign key with alias "${alias}" found. Available aliases: ${availableAliases}`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
ref = found;
|
|
1058
|
+
} else {
|
|
1059
|
+
const availableAliases = refs.map((r) => `"${r.as}"`).join(", ");
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`Multiple foreign keys from "${referencingTable.name}" to "${name}". Specify an alias: ${availableAliases}`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
const tableRef = alias ?? name;
|
|
1065
|
+
return createTemplate(makeTemplate(["", ".", " = ", ".", ""]), [
|
|
1066
|
+
ident(tableRef),
|
|
1067
|
+
ident(ref.referencedField),
|
|
1068
|
+
ident(referencingTable.name),
|
|
1069
|
+
ident(ref.fieldName)
|
|
1070
|
+
]);
|
|
1071
|
+
},
|
|
1072
|
+
values(rows) {
|
|
1073
|
+
if (rows.length === 0) {
|
|
1074
|
+
throw new Error("values() requires at least one row");
|
|
1075
|
+
}
|
|
1076
|
+
const columns = Object.keys(rows[0]);
|
|
1077
|
+
if (columns.length === 0) {
|
|
1078
|
+
throw new Error("values() requires at least one column");
|
|
1079
|
+
}
|
|
1080
|
+
const schemaKeys = Object.keys(schema.shape);
|
|
1081
|
+
for (const col of columns) {
|
|
1082
|
+
if (!schemaKeys.includes(col)) {
|
|
1083
|
+
throw new TableDefinitionError(
|
|
1084
|
+
`Column "${col}" does not exist in table schema`,
|
|
1085
|
+
name,
|
|
1086
|
+
col
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const partialSchema = schema.partial();
|
|
1091
|
+
const strings = ["("];
|
|
1092
|
+
const templateValues = [];
|
|
1093
|
+
for (let i = 0; i < columns.length; i++) {
|
|
1094
|
+
templateValues.push(ident(columns[i]));
|
|
1095
|
+
strings.push(i < columns.length - 1 ? ", " : ") VALUES ");
|
|
1096
|
+
}
|
|
1097
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
1098
|
+
const row = rows[rowIdx];
|
|
1099
|
+
const validated = validateWithStandardSchema(partialSchema, row);
|
|
1100
|
+
strings[strings.length - 1] += "(";
|
|
1101
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
1102
|
+
const col = columns[colIdx];
|
|
1103
|
+
if (!(col in row)) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`All rows must have the same columns. Row is missing column "${col}"`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
templateValues.push(validated[col]);
|
|
1109
|
+
strings.push(colIdx < columns.length - 1 ? ", " : ")");
|
|
1110
|
+
}
|
|
1111
|
+
if (rowIdx < rows.length - 1) {
|
|
1112
|
+
strings[strings.length - 1] += ", ";
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return createTemplate(makeTemplate(strings), templateValues);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
return table2;
|
|
1119
|
+
}
|
|
1120
|
+
function getLayerMeta(schema) {
|
|
1121
|
+
if (typeof schema.meta === "function") {
|
|
1122
|
+
return schema.meta() ?? {};
|
|
1123
|
+
}
|
|
1124
|
+
return {};
|
|
1125
|
+
}
|
|
1126
|
+
function unwrapType(schema) {
|
|
1127
|
+
let core = schema;
|
|
1128
|
+
let hasDefault = false;
|
|
1129
|
+
let defaultValue = void 0;
|
|
1130
|
+
const metaLayers = [];
|
|
1131
|
+
const isOptional = schema.isOptional();
|
|
1132
|
+
const isNullable = schema.isNullable();
|
|
1133
|
+
while (true) {
|
|
1134
|
+
metaLayers.push(getLayerMeta(core));
|
|
1135
|
+
if (typeof core.removeDefault === "function") {
|
|
1136
|
+
hasDefault = true;
|
|
1137
|
+
try {
|
|
1138
|
+
defaultValue = core.parse(void 0);
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
core = core.removeDefault();
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (typeof core.unwrap === "function") {
|
|
1145
|
+
core = core.unwrap();
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof core.innerType === "function") {
|
|
1149
|
+
core = core.innerType();
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
const collectedMeta = {};
|
|
1155
|
+
for (let i = metaLayers.length - 1; i >= 0; i--) {
|
|
1156
|
+
Object.assign(collectedMeta, metaLayers[i]);
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
core,
|
|
1160
|
+
isOptional,
|
|
1161
|
+
isNullable,
|
|
1162
|
+
hasDefault,
|
|
1163
|
+
defaultValue,
|
|
1164
|
+
collectedMeta
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function extractFieldMeta(name, zodType, dbMeta) {
|
|
1168
|
+
const {
|
|
1169
|
+
core,
|
|
1170
|
+
isOptional,
|
|
1171
|
+
isNullable,
|
|
1172
|
+
hasDefault,
|
|
1173
|
+
defaultValue,
|
|
1174
|
+
collectedMeta
|
|
1175
|
+
} = unwrapType(zodType);
|
|
1176
|
+
const { db: _db, ...userMeta } = collectedMeta;
|
|
1177
|
+
const meta = {
|
|
1178
|
+
name,
|
|
1179
|
+
type: "text",
|
|
1180
|
+
required: !isOptional && !isNullable && !hasDefault,
|
|
1181
|
+
...userMeta
|
|
1182
|
+
// Spread user-defined metadata (label, helpText, widget, etc.)
|
|
1183
|
+
};
|
|
1184
|
+
if (dbMeta.primaryKey)
|
|
1185
|
+
meta.primaryKey = true;
|
|
1186
|
+
if (dbMeta.unique)
|
|
1187
|
+
meta.unique = true;
|
|
1188
|
+
if (dbMeta.indexed)
|
|
1189
|
+
meta.indexed = true;
|
|
1190
|
+
if (dbMeta.softDelete)
|
|
1191
|
+
meta.softDelete = true;
|
|
1192
|
+
if (dbMeta.reference) {
|
|
1193
|
+
meta.reference = {
|
|
1194
|
+
table: dbMeta.reference.table,
|
|
1195
|
+
field: dbMeta.reference.field ?? getTableMeta(dbMeta.reference.table).primary ?? "id",
|
|
1196
|
+
as: dbMeta.reference.as,
|
|
1197
|
+
onDelete: dbMeta.reference.onDelete
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
if (dbMeta.encode)
|
|
1201
|
+
meta.encode = dbMeta.encode;
|
|
1202
|
+
if (dbMeta.decode)
|
|
1203
|
+
meta.decode = dbMeta.decode;
|
|
1204
|
+
if (dbMeta.columnType)
|
|
1205
|
+
meta.columnType = dbMeta.columnType;
|
|
1206
|
+
if (dbMeta.autoIncrement)
|
|
1207
|
+
meta.autoIncrement = true;
|
|
1208
|
+
if (dbMeta.inserted)
|
|
1209
|
+
meta.inserted = dbMeta.inserted;
|
|
1210
|
+
if (dbMeta.updated)
|
|
1211
|
+
meta.updated = dbMeta.updated;
|
|
1212
|
+
if (dbMeta.upserted)
|
|
1213
|
+
meta.upserted = dbMeta.upserted;
|
|
1214
|
+
if (defaultValue !== void 0) {
|
|
1215
|
+
meta.default = defaultValue;
|
|
1216
|
+
}
|
|
1217
|
+
if (core instanceof z.ZodString) {
|
|
1218
|
+
meta.type = "text";
|
|
1219
|
+
const str = core;
|
|
1220
|
+
if (str.format === "email" || str.isEmail)
|
|
1221
|
+
meta.type = "email";
|
|
1222
|
+
if (str.format === "url" || str.isURL)
|
|
1223
|
+
meta.type = "url";
|
|
1224
|
+
if (str.maxLength !== void 0) {
|
|
1225
|
+
meta.maxLength = str.maxLength;
|
|
1226
|
+
if (str.maxLength > 500)
|
|
1227
|
+
meta.type = "textarea";
|
|
1228
|
+
}
|
|
1229
|
+
if (str.minLength !== void 0) {
|
|
1230
|
+
meta.minLength = str.minLength;
|
|
1231
|
+
}
|
|
1232
|
+
} else if (core instanceof z.ZodNumber) {
|
|
1233
|
+
meta.type = "number";
|
|
1234
|
+
const num = core;
|
|
1235
|
+
if (num.format === "int" || num.isInt)
|
|
1236
|
+
meta.type = "integer";
|
|
1237
|
+
if (num.minValue !== void 0)
|
|
1238
|
+
meta.min = num.minValue;
|
|
1239
|
+
if (num.maxValue !== void 0)
|
|
1240
|
+
meta.max = num.maxValue;
|
|
1241
|
+
} else if (core instanceof z.ZodBoolean) {
|
|
1242
|
+
meta.type = "checkbox";
|
|
1243
|
+
} else if (core instanceof z.ZodDate) {
|
|
1244
|
+
meta.type = "datetime";
|
|
1245
|
+
} else if (core instanceof z.ZodEnum) {
|
|
1246
|
+
meta.type = "select";
|
|
1247
|
+
meta.options = core.options;
|
|
1248
|
+
} else if (core instanceof z.ZodArray || core instanceof z.ZodObject) {
|
|
1249
|
+
meta.type = "json";
|
|
1250
|
+
}
|
|
1251
|
+
return meta;
|
|
1252
|
+
}
|
|
1253
|
+
function decodeData(table2, data) {
|
|
1254
|
+
if (!data)
|
|
1255
|
+
return data;
|
|
1256
|
+
const decoded = {};
|
|
1257
|
+
const shape = table2.schema.shape;
|
|
1258
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1259
|
+
const fieldMeta = getTableMeta(table2).fields[key];
|
|
1260
|
+
const fieldSchema = shape?.[key];
|
|
1261
|
+
if (fieldMeta?.decode && typeof fieldMeta.decode === "function") {
|
|
1262
|
+
decoded[key] = fieldMeta.decode(value);
|
|
1263
|
+
} else if (fieldSchema) {
|
|
1264
|
+
let core = fieldSchema;
|
|
1265
|
+
while (typeof core.unwrap === "function") {
|
|
1266
|
+
if (core instanceof z.ZodArray || core instanceof z.ZodObject) {
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
core = core.unwrap();
|
|
1270
|
+
}
|
|
1271
|
+
if (core instanceof z.ZodObject || core instanceof z.ZodArray) {
|
|
1272
|
+
if (typeof value === "string") {
|
|
1273
|
+
try {
|
|
1274
|
+
decoded[key] = JSON.parse(value);
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
throw new Error(
|
|
1277
|
+
`JSON parse error for field "${key}": ${e instanceof Error ? e.message : String(e)}. Value was: ${value.slice(0, 100)}${value.length > 100 ? "..." : ""}`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
decoded[key] = value;
|
|
1282
|
+
}
|
|
1283
|
+
} else if (core instanceof z.ZodDate) {
|
|
1284
|
+
if (typeof value === "string") {
|
|
1285
|
+
const date = new Date(value);
|
|
1286
|
+
if (isNaN(date.getTime())) {
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
`Invalid date value for field "${key}": "${value}" cannot be parsed as a valid date`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
decoded[key] = date;
|
|
1292
|
+
} else if (value instanceof Date) {
|
|
1293
|
+
if (isNaN(value.getTime())) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Invalid Date object for field "${key}": received an Invalid Date`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
decoded[key] = value;
|
|
1299
|
+
} else {
|
|
1300
|
+
decoded[key] = value;
|
|
1301
|
+
}
|
|
1302
|
+
} else {
|
|
1303
|
+
decoded[key] = value;
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
decoded[key] = value;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return decoded;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export {
|
|
1313
|
+
DatabaseError,
|
|
1314
|
+
ValidationError,
|
|
1315
|
+
TableDefinitionError,
|
|
1316
|
+
MigrationError,
|
|
1317
|
+
MigrationLockError,
|
|
1318
|
+
QueryError,
|
|
1319
|
+
NotFoundError,
|
|
1320
|
+
AlreadyExistsError,
|
|
1321
|
+
ConstraintViolationError,
|
|
1322
|
+
ConnectionError,
|
|
1323
|
+
TransactionError,
|
|
1324
|
+
EnsureError,
|
|
1325
|
+
SchemaDriftError,
|
|
1326
|
+
ConstraintPreflightError,
|
|
1327
|
+
isDatabaseError,
|
|
1328
|
+
hasErrorCode,
|
|
1329
|
+
CURRENT_TIMESTAMP,
|
|
1330
|
+
CURRENT_DATE,
|
|
1331
|
+
CURRENT_TIME,
|
|
1332
|
+
NOW,
|
|
1333
|
+
TODAY,
|
|
1334
|
+
isSQLBuiltin,
|
|
1335
|
+
resolveSQLBuiltin,
|
|
1336
|
+
createTemplate,
|
|
1337
|
+
isSQLTemplate,
|
|
1338
|
+
ident,
|
|
1339
|
+
isSQLIdentifier,
|
|
1340
|
+
makeTemplate,
|
|
1341
|
+
validateWithStandardSchema,
|
|
1342
|
+
extendZod,
|
|
1343
|
+
getTableMeta,
|
|
1344
|
+
table,
|
|
1345
|
+
decodeData
|
|
1346
|
+
};
|