@b9g/zen 0.1.0 → 0.1.1
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 +23 -0
- package/README.md +32 -12
- package/{chunk-QXGEP5PB.js → chunk-CHF7L5PC.js} +27 -2
- package/chunk-W7JTNEM4.js +63 -0
- package/{chunk-56M5Z3A6.js → chunk-XHXMCOSW.js} +176 -4
- package/ddl-2A2UFUR3.js +11 -0
- package/package.json +1 -1
- package/src/bun.d.ts +12 -1
- package/src/bun.js +137 -5
- package/src/mysql.d.ts +12 -0
- package/src/mysql.js +121 -4
- package/src/postgres.d.ts +12 -0
- package/src/postgres.js +101 -4
- package/src/sqlite.d.ts +12 -1
- package/src/sqlite.js +113 -4
- package/src/zen.d.ts +1 -1
- package/src/zen.js +190 -49
- package/chunk-2IEEEMRN.js +0 -38
- package/ddl-NAJM37GQ.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.1] - 2025-12-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Driver-level type encoding/decoding for dialect-specific handling
|
|
13
|
+
- `encodeValue(value, fieldType)` and `decodeValue(value, fieldType)` methods on Driver interface
|
|
14
|
+
- SQLite: Date→ISO string, boolean→1/0, JSON stringify/parse
|
|
15
|
+
- MySQL: Date→"YYYY-MM-DD HH:MM:SS", boolean→1/0, JSON stringify/parse
|
|
16
|
+
- PostgreSQL: Mostly passthrough (pg handles natively), JSON stringify
|
|
17
|
+
- `inferFieldType()` helper to infer field type from Zod schema
|
|
18
|
+
- Node.js tests for encode/decode functionality
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Breaking:** Removed deprecated `Infer<T>` type alias (use `Row<T>` instead)
|
|
23
|
+
- Renamed internal types for clarity:
|
|
24
|
+
- `InferRefs` → `RowRefs`
|
|
25
|
+
- `WithRefs` → `JoinedRow`
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Invalid datetime values now throw errors instead of returning Invalid Date
|
|
30
|
+
|
|
8
31
|
## [0.1.0] - 2025-12-20
|
|
9
32
|
|
|
10
33
|
Initial release of @b9g/zen - the simple database client.
|
package/README.md
CHANGED
|
@@ -253,7 +253,14 @@ await db.delete(Users, userId);
|
|
|
253
253
|
const activeUsers = await db.all(Users)`
|
|
254
254
|
WHERE NOT ${Users.deleted()}
|
|
255
255
|
`;
|
|
256
|
-
|
|
256
|
+
|
|
257
|
+
// Or use the .active view (auto-generated, read-only)
|
|
258
|
+
const activeUsers = await db.all(Users.active)``;
|
|
259
|
+
|
|
260
|
+
// JOINs with .active automatically filter deleted rows
|
|
261
|
+
const posts = await db.all([Posts, Users.active])`
|
|
262
|
+
JOIN "users_active" ON ${Users.active.cols.id} = ${Posts.cols.authorId}
|
|
263
|
+
`;
|
|
257
264
|
```
|
|
258
265
|
|
|
259
266
|
**Compound indexes** via table options:
|
|
@@ -815,17 +822,27 @@ console.log(Posts.ddl().toString());
|
|
|
815
822
|
|
|
816
823
|
| Feature | SQLite | PostgreSQL | MySQL |
|
|
817
824
|
|---------|--------|------------|-------|
|
|
818
|
-
|
|
|
819
|
-
|
|
|
820
|
-
|
|
|
821
|
-
|
|
|
822
|
-
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
|
827
|
-
|
|
828
|
-
|
|
|
825
|
+
| RETURNING | ✅ | ✅ | ⚠️ fallback |
|
|
826
|
+
| IF NOT EXISTS (CREATE TABLE) | ✅ | ✅ | ✅ |
|
|
827
|
+
| IF NOT EXISTS (ADD COLUMN) | ✅ | ✅ | ⚠️ may error |
|
|
828
|
+
| Migration Locks | BEGIN EXCLUSIVE | pg_advisory_lock | GET_LOCK |
|
|
829
|
+
| Advisory Locks | — | ✅ | ✅ |
|
|
830
|
+
|
|
831
|
+
### Zod to SQL Type Mapping
|
|
832
|
+
|
|
833
|
+
| Zod Type | SQLite | PostgreSQL | MySQL |
|
|
834
|
+
|----------|--------|------------|-------|
|
|
835
|
+
| `z.string()` | TEXT | TEXT | TEXT |
|
|
836
|
+
| `z.string().max(n)` (n ≤ 255) | TEXT | VARCHAR(n) | VARCHAR(n) |
|
|
837
|
+
| `z.number()` | REAL | DOUBLE PRECISION | REAL |
|
|
838
|
+
| `z.number().int()` | INTEGER | INTEGER | INTEGER |
|
|
839
|
+
| `z.boolean()` | INTEGER | BOOLEAN | BOOLEAN |
|
|
840
|
+
| `z.date()` | TEXT | TIMESTAMPTZ | DATETIME |
|
|
841
|
+
| `z.enum([...])` | TEXT | TEXT | TEXT |
|
|
842
|
+
| `z.object({...})` | TEXT | JSONB | TEXT |
|
|
843
|
+
| `z.array(...)` | TEXT | JSONB | TEXT |
|
|
844
|
+
|
|
845
|
+
Override with `.db.type("CUSTOM")` when using custom encode/decode.
|
|
829
846
|
|
|
830
847
|
## Public API Reference
|
|
831
848
|
|
|
@@ -961,6 +978,9 @@ Users.pick("id", "email"); // PartialTable with subset of fields
|
|
|
961
978
|
Users.derive("hasEmail", z.boolean())`
|
|
962
979
|
${Users.cols.email} IS NOT NULL
|
|
963
980
|
`;
|
|
981
|
+
|
|
982
|
+
// Views
|
|
983
|
+
Users.active; // View excluding soft-deleted rows (read-only)
|
|
964
984
|
```
|
|
965
985
|
|
|
966
986
|
### Database Methods
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTemplate,
|
|
3
3
|
getTableMeta,
|
|
4
|
+
getViewMeta,
|
|
4
5
|
ident,
|
|
5
6
|
makeTemplate
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XHXMCOSW.js";
|
|
7
8
|
|
|
8
9
|
// src/impl/ddl.ts
|
|
9
10
|
import { z } from "zod";
|
|
@@ -303,8 +304,32 @@ CREATE INDEX ${indexExists}`;
|
|
|
303
304
|
}
|
|
304
305
|
return createTemplate(makeTemplate(strings), values);
|
|
305
306
|
}
|
|
307
|
+
function generateViewDDL(viewObj, _options = {}) {
|
|
308
|
+
const viewMeta = getViewMeta(viewObj);
|
|
309
|
+
const strings = [];
|
|
310
|
+
const values = [];
|
|
311
|
+
strings.push("DROP VIEW IF EXISTS ");
|
|
312
|
+
values.push(ident(viewObj.name));
|
|
313
|
+
strings.push(";\n\n");
|
|
314
|
+
strings[strings.length - 1] += "CREATE VIEW ";
|
|
315
|
+
values.push(ident(viewObj.name));
|
|
316
|
+
strings.push(" AS SELECT * FROM ");
|
|
317
|
+
values.push(ident(viewMeta.baseTable.name));
|
|
318
|
+
strings.push(" ");
|
|
319
|
+
const whereTemplate = viewMeta.whereTemplate;
|
|
320
|
+
const whereStrings = whereTemplate[0];
|
|
321
|
+
const whereValues = whereTemplate.slice(1);
|
|
322
|
+
strings[strings.length - 1] += whereStrings[0];
|
|
323
|
+
for (let i = 0; i < whereValues.length; i++) {
|
|
324
|
+
values.push(whereValues[i]);
|
|
325
|
+
strings.push(whereStrings[i + 1]);
|
|
326
|
+
}
|
|
327
|
+
strings[strings.length - 1] += ";";
|
|
328
|
+
return createTemplate(makeTemplate(strings), values);
|
|
329
|
+
}
|
|
306
330
|
|
|
307
331
|
export {
|
|
308
332
|
generateColumnDDL,
|
|
309
|
-
generateDDL
|
|
333
|
+
generateDDL,
|
|
334
|
+
generateViewDDL
|
|
310
335
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSQLBuiltin,
|
|
3
|
+
isSQLIdentifier,
|
|
4
|
+
resolveSQLBuiltin
|
|
5
|
+
} from "./chunk-XHXMCOSW.js";
|
|
6
|
+
|
|
7
|
+
// src/impl/sql.ts
|
|
8
|
+
function quoteIdent(name, dialect) {
|
|
9
|
+
if (dialect === "mysql") {
|
|
10
|
+
return `\`${name.replace(/`/g, "``")}\``;
|
|
11
|
+
}
|
|
12
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
13
|
+
}
|
|
14
|
+
function placeholder(index, dialect) {
|
|
15
|
+
if (dialect === "postgresql") {
|
|
16
|
+
return `$${index}`;
|
|
17
|
+
}
|
|
18
|
+
return "?";
|
|
19
|
+
}
|
|
20
|
+
function renderDDL(strings, values, dialect) {
|
|
21
|
+
let sql = "";
|
|
22
|
+
for (let i = 0; i < strings.length; i++) {
|
|
23
|
+
sql += strings[i];
|
|
24
|
+
if (i < values.length) {
|
|
25
|
+
const value = values[i];
|
|
26
|
+
if (isSQLBuiltin(value)) {
|
|
27
|
+
sql += resolveSQLBuiltin(value);
|
|
28
|
+
} else if (isSQLIdentifier(value)) {
|
|
29
|
+
sql += quoteIdent(value.name, dialect);
|
|
30
|
+
} else {
|
|
31
|
+
sql += inlineLiteral(value, dialect);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return sql;
|
|
36
|
+
}
|
|
37
|
+
function inlineLiteral(value, dialect) {
|
|
38
|
+
if (value === null || value === void 0) {
|
|
39
|
+
return "NULL";
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "boolean") {
|
|
42
|
+
if (dialect === "sqlite") {
|
|
43
|
+
return value ? "1" : "0";
|
|
44
|
+
}
|
|
45
|
+
return value ? "TRUE" : "FALSE";
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "number") {
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
52
|
+
}
|
|
53
|
+
if (value instanceof Date) {
|
|
54
|
+
return `'${value.toISOString()}'`;
|
|
55
|
+
}
|
|
56
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
quoteIdent,
|
|
61
|
+
placeholder,
|
|
62
|
+
renderDDL
|
|
63
|
+
};
|
|
@@ -668,6 +668,120 @@ var TABLE_META = Symbol.for("@b9g/zen:table-meta");
|
|
|
668
668
|
function getTableMeta(table2) {
|
|
669
669
|
return table2[TABLE_META];
|
|
670
670
|
}
|
|
671
|
+
var VIEW_MARKER = Symbol("view");
|
|
672
|
+
var VIEW_META = Symbol("viewMeta");
|
|
673
|
+
function isView(value) {
|
|
674
|
+
return typeof value === "object" && value !== null && value[VIEW_MARKER] === true;
|
|
675
|
+
}
|
|
676
|
+
function getViewMeta(view2) {
|
|
677
|
+
return view2[VIEW_META];
|
|
678
|
+
}
|
|
679
|
+
function view(name, baseTable) {
|
|
680
|
+
validateIdentifier(name, "table");
|
|
681
|
+
if (name.includes(".")) {
|
|
682
|
+
throw new TableDefinitionError(
|
|
683
|
+
`Invalid view name "${name}": view names cannot contain "." as it conflicts with normalization prefixes`,
|
|
684
|
+
name
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return (strings, ...templateValues) => {
|
|
688
|
+
const resultStrings = [];
|
|
689
|
+
const resultValues = [];
|
|
690
|
+
for (let i = 0; i < strings.length; i++) {
|
|
691
|
+
if (i === 0) {
|
|
692
|
+
resultStrings.push(strings[i]);
|
|
693
|
+
}
|
|
694
|
+
if (i < templateValues.length) {
|
|
695
|
+
const value = templateValues[i];
|
|
696
|
+
if (isSQLTemplate(value)) {
|
|
697
|
+
mergeFragment(resultStrings, resultValues, value, strings[i + 1]);
|
|
698
|
+
} else {
|
|
699
|
+
resultValues.push(value);
|
|
700
|
+
resultStrings.push(strings[i + 1]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (resultStrings.length > 0) {
|
|
705
|
+
resultStrings[0] = resultStrings[0].trimStart();
|
|
706
|
+
resultStrings[resultStrings.length - 1] = resultStrings[resultStrings.length - 1].trimEnd();
|
|
707
|
+
}
|
|
708
|
+
const whereTemplate = createTemplate(
|
|
709
|
+
makeTemplate(resultStrings),
|
|
710
|
+
resultValues
|
|
711
|
+
);
|
|
712
|
+
return createViewObject(name, baseTable, whereTemplate);
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function createViewObject(name, baseTable, whereTemplate) {
|
|
716
|
+
const baseMeta = getTableMeta(baseTable);
|
|
717
|
+
const cols = new Proxy({}, {
|
|
718
|
+
get(_target, prop) {
|
|
719
|
+
if (prop in baseTable.schema.shape) {
|
|
720
|
+
return createTemplate(makeTemplate(["", ".", ""]), [
|
|
721
|
+
ident(name),
|
|
722
|
+
ident(prop)
|
|
723
|
+
]);
|
|
724
|
+
}
|
|
725
|
+
return void 0;
|
|
726
|
+
},
|
|
727
|
+
has(_target, prop) {
|
|
728
|
+
return prop in baseTable.schema.shape;
|
|
729
|
+
},
|
|
730
|
+
ownKeys() {
|
|
731
|
+
return Object.keys(baseTable.schema.shape);
|
|
732
|
+
},
|
|
733
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
734
|
+
if (prop in baseTable.schema.shape) {
|
|
735
|
+
return { enumerable: true, configurable: true };
|
|
736
|
+
}
|
|
737
|
+
return void 0;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
const primary = baseMeta.primary ? createTemplate(makeTemplate(["", ".", ""]), [
|
|
741
|
+
ident(name),
|
|
742
|
+
ident(baseMeta.primary)
|
|
743
|
+
]) : null;
|
|
744
|
+
const viewMeta = {
|
|
745
|
+
baseTable,
|
|
746
|
+
whereTemplate
|
|
747
|
+
};
|
|
748
|
+
const tableMeta = {
|
|
749
|
+
...baseMeta,
|
|
750
|
+
isView: true,
|
|
751
|
+
viewOf: baseTable.name,
|
|
752
|
+
derivedExprs: void 0,
|
|
753
|
+
derivedFields: void 0,
|
|
754
|
+
isDerived: void 0
|
|
755
|
+
};
|
|
756
|
+
const viewObj = {
|
|
757
|
+
name,
|
|
758
|
+
schema: baseTable.schema,
|
|
759
|
+
meta: tableMeta,
|
|
760
|
+
cols,
|
|
761
|
+
primary,
|
|
762
|
+
baseTable,
|
|
763
|
+
fields() {
|
|
764
|
+
return baseTable.fields();
|
|
765
|
+
},
|
|
766
|
+
references() {
|
|
767
|
+
return baseTable.references();
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
Object.defineProperty(viewObj, VIEW_MARKER, { value: true, enumerable: false });
|
|
771
|
+
Object.defineProperty(viewObj, VIEW_META, {
|
|
772
|
+
value: viewMeta,
|
|
773
|
+
enumerable: false
|
|
774
|
+
});
|
|
775
|
+
Object.defineProperty(viewObj, TABLE_MARKER, {
|
|
776
|
+
value: true,
|
|
777
|
+
enumerable: false
|
|
778
|
+
});
|
|
779
|
+
Object.defineProperty(viewObj, TABLE_META, {
|
|
780
|
+
value: tableMeta,
|
|
781
|
+
enumerable: false
|
|
782
|
+
});
|
|
783
|
+
return viewObj;
|
|
784
|
+
}
|
|
671
785
|
function table(name, shape, options = {}) {
|
|
672
786
|
validateIdentifier(name, "table");
|
|
673
787
|
if (name.includes(".")) {
|
|
@@ -886,7 +1000,7 @@ function createTableObject(name, schema, zodShape, meta, options) {
|
|
|
886
1000
|
`Table "${name}" does not have a soft delete field. Use softDelete() wrapper to mark a field.`
|
|
887
1001
|
);
|
|
888
1002
|
}
|
|
889
|
-
return createTemplate(makeTemplate(["", ".", " IS NOT NULL"]), [
|
|
1003
|
+
return createTemplate(makeTemplate(["(", ".", " IS NOT NULL)"]), [
|
|
890
1004
|
ident(name),
|
|
891
1005
|
ident(softDeleteField)
|
|
892
1006
|
]);
|
|
@@ -1113,6 +1227,28 @@ function createTableObject(name, schema, zodShape, meta, options) {
|
|
|
1113
1227
|
}
|
|
1114
1228
|
}
|
|
1115
1229
|
return createTemplate(makeTemplate(strings), templateValues);
|
|
1230
|
+
},
|
|
1231
|
+
get active() {
|
|
1232
|
+
const softDeleteField = meta.softDeleteField;
|
|
1233
|
+
if (!softDeleteField) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`Table "${name}" does not have a soft delete field. Use .db.softDelete() to mark a field for soft delete.`
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const whereTemplate = createTemplate(
|
|
1239
|
+
makeTemplate(["WHERE ", ".", " IS NULL"]),
|
|
1240
|
+
[ident(name), ident(softDeleteField)]
|
|
1241
|
+
);
|
|
1242
|
+
const activeViewName = `${name}_active`;
|
|
1243
|
+
const activeView = createViewObject(
|
|
1244
|
+
activeViewName,
|
|
1245
|
+
table2,
|
|
1246
|
+
whereTemplate
|
|
1247
|
+
);
|
|
1248
|
+
if (!internalMeta.activeView) {
|
|
1249
|
+
internalMeta.activeView = activeView;
|
|
1250
|
+
}
|
|
1251
|
+
return activeView;
|
|
1116
1252
|
}
|
|
1117
1253
|
};
|
|
1118
1254
|
return table2;
|
|
@@ -1250,7 +1386,25 @@ function extractFieldMeta(name, zodType, dbMeta) {
|
|
|
1250
1386
|
}
|
|
1251
1387
|
return meta;
|
|
1252
1388
|
}
|
|
1253
|
-
function
|
|
1389
|
+
function inferFieldType(schema) {
|
|
1390
|
+
let core = schema;
|
|
1391
|
+
while (typeof core.unwrap === "function") {
|
|
1392
|
+
if (core instanceof z.ZodArray || core instanceof z.ZodObject || core instanceof z.ZodDate || core instanceof z.ZodBoolean || core instanceof z.ZodNumber) {
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
core = core.unwrap();
|
|
1396
|
+
}
|
|
1397
|
+
if (core instanceof z.ZodDate)
|
|
1398
|
+
return "datetime";
|
|
1399
|
+
if (core instanceof z.ZodBoolean)
|
|
1400
|
+
return "boolean";
|
|
1401
|
+
if (core instanceof z.ZodObject || core instanceof z.ZodArray)
|
|
1402
|
+
return "json";
|
|
1403
|
+
if (core instanceof z.ZodNumber)
|
|
1404
|
+
return "real";
|
|
1405
|
+
return "text";
|
|
1406
|
+
}
|
|
1407
|
+
function decodeData(table2, data, driver) {
|
|
1254
1408
|
if (!data)
|
|
1255
1409
|
return data;
|
|
1256
1410
|
const decoded = {};
|
|
@@ -1260,10 +1414,13 @@ function decodeData(table2, data) {
|
|
|
1260
1414
|
const fieldSchema = shape?.[key];
|
|
1261
1415
|
if (fieldMeta?.decode && typeof fieldMeta.decode === "function") {
|
|
1262
1416
|
decoded[key] = fieldMeta.decode(value);
|
|
1417
|
+
} else if (driver?.decodeValue && fieldSchema) {
|
|
1418
|
+
const fieldType = inferFieldType(fieldSchema);
|
|
1419
|
+
decoded[key] = driver.decodeValue(value, fieldType);
|
|
1263
1420
|
} else if (fieldSchema) {
|
|
1264
1421
|
let core = fieldSchema;
|
|
1265
1422
|
while (typeof core.unwrap === "function") {
|
|
1266
|
-
if (core instanceof z.ZodArray || core instanceof z.ZodObject) {
|
|
1423
|
+
if (core instanceof z.ZodArray || core instanceof z.ZodObject || core instanceof z.ZodBoolean || core instanceof z.ZodDate) {
|
|
1267
1424
|
break;
|
|
1268
1425
|
}
|
|
1269
1426
|
core = core.unwrap();
|
|
@@ -1280,9 +1437,20 @@ function decodeData(table2, data) {
|
|
|
1280
1437
|
} else {
|
|
1281
1438
|
decoded[key] = value;
|
|
1282
1439
|
}
|
|
1440
|
+
} else if (core instanceof z.ZodBoolean) {
|
|
1441
|
+
if (typeof value === "number") {
|
|
1442
|
+
decoded[key] = value !== 0;
|
|
1443
|
+
} else if (typeof value === "string") {
|
|
1444
|
+
decoded[key] = value !== "0" && value !== "";
|
|
1445
|
+
} else if (typeof value === "boolean") {
|
|
1446
|
+
decoded[key] = value;
|
|
1447
|
+
} else {
|
|
1448
|
+
decoded[key] = value;
|
|
1449
|
+
}
|
|
1283
1450
|
} else if (core instanceof z.ZodDate) {
|
|
1284
1451
|
if (typeof value === "string") {
|
|
1285
|
-
const
|
|
1452
|
+
const normalized = value.includes("T") ? value : value.replace(" ", "T") + "Z";
|
|
1453
|
+
const date = new Date(normalized);
|
|
1286
1454
|
if (isNaN(date.getTime())) {
|
|
1287
1455
|
throw new Error(
|
|
1288
1456
|
`Invalid date value for field "${key}": "${value}" cannot be parsed as a valid date`
|
|
@@ -1341,6 +1509,10 @@ export {
|
|
|
1341
1509
|
validateWithStandardSchema,
|
|
1342
1510
|
extendZod,
|
|
1343
1511
|
getTableMeta,
|
|
1512
|
+
isView,
|
|
1513
|
+
getViewMeta,
|
|
1514
|
+
view,
|
|
1344
1515
|
table,
|
|
1516
|
+
inferFieldType,
|
|
1345
1517
|
decodeData
|
|
1346
1518
|
};
|
package/ddl-2A2UFUR3.js
ADDED
package/package.json
CHANGED
package/src/bun.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Unified driver supporting PostgreSQL, MySQL, and SQLite via Bun's built-in SQL.
|
|
5
5
|
* Zero dependencies - uses native Bun implementation.
|
|
6
6
|
*/
|
|
7
|
-
import type { Driver, Table, EnsureResult } from "./zen.js";
|
|
7
|
+
import type { Driver, Table, View, EnsureResult } from "./zen.js";
|
|
8
8
|
/**
|
|
9
9
|
* Bun driver using Bun's built-in SQL.
|
|
10
10
|
* Supports PostgreSQL, MySQL, and SQLite with automatic dialect detection.
|
|
@@ -34,9 +34,20 @@ export default class BunDriver implements Driver {
|
|
|
34
34
|
run(strings: TemplateStringsArray, values: unknown[]): Promise<number>;
|
|
35
35
|
val<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
|
|
36
36
|
close(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Encode a JS value for database insertion.
|
|
39
|
+
* Dialect-aware: PostgreSQL uses native types, SQLite/MySQL need conversion.
|
|
40
|
+
*/
|
|
41
|
+
encodeValue(value: unknown, fieldType: string): unknown;
|
|
42
|
+
/**
|
|
43
|
+
* Decode a database value to JS.
|
|
44
|
+
* Dialect-aware: handles differences in how values are returned.
|
|
45
|
+
*/
|
|
46
|
+
decodeValue(value: unknown, fieldType: string): unknown;
|
|
37
47
|
transaction<T>(fn: (txDriver: Driver) => Promise<T>): Promise<T>;
|
|
38
48
|
withMigrationLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
39
49
|
ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
50
|
+
ensureView<T extends View<any>>(viewObj: T): Promise<EnsureResult>;
|
|
40
51
|
ensureConstraints<T extends Table<any>>(table: T): Promise<EnsureResult>;
|
|
41
52
|
/**
|
|
42
53
|
* Optional introspection: list columns for a table.
|
package/src/bun.js
CHANGED
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
placeholder,
|
|
4
4
|
quoteIdent,
|
|
5
5
|
renderDDL
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-W7JTNEM4.js";
|
|
7
7
|
import {
|
|
8
|
-
generateDDL
|
|
9
|
-
|
|
8
|
+
generateDDL,
|
|
9
|
+
generateViewDDL
|
|
10
|
+
} from "../chunk-CHF7L5PC.js";
|
|
10
11
|
import {
|
|
12
|
+
getTableMeta,
|
|
11
13
|
resolveSQLBuiltin
|
|
12
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-XHXMCOSW.js";
|
|
13
15
|
|
|
14
16
|
// src/bun.ts
|
|
15
17
|
import { SQL } from "bun";
|
|
@@ -227,13 +229,103 @@ var BunDriver = class {
|
|
|
227
229
|
async close() {
|
|
228
230
|
await this.#sql.close();
|
|
229
231
|
}
|
|
232
|
+
// ==========================================================================
|
|
233
|
+
// Type Encoding/Decoding
|
|
234
|
+
// ==========================================================================
|
|
235
|
+
/**
|
|
236
|
+
* Encode a JS value for database insertion.
|
|
237
|
+
* Dialect-aware: PostgreSQL uses native types, SQLite/MySQL need conversion.
|
|
238
|
+
*/
|
|
239
|
+
encodeValue(value, fieldType) {
|
|
240
|
+
if (value === null || value === void 0) {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
switch (fieldType) {
|
|
244
|
+
case "datetime":
|
|
245
|
+
if (value instanceof Date && !isNaN(value.getTime())) {
|
|
246
|
+
if (this.#dialect === "postgresql") {
|
|
247
|
+
return value;
|
|
248
|
+
} else if (this.#dialect === "mysql") {
|
|
249
|
+
return value.toISOString().replace("T", " ").slice(0, 23);
|
|
250
|
+
} else {
|
|
251
|
+
return value.toISOString();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
case "boolean":
|
|
256
|
+
if (this.#dialect === "postgresql") {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
return value ? 1 : 0;
|
|
260
|
+
case "json":
|
|
261
|
+
return JSON.stringify(value);
|
|
262
|
+
default:
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Decode a database value to JS.
|
|
268
|
+
* Dialect-aware: handles differences in how values are returned.
|
|
269
|
+
*/
|
|
270
|
+
decodeValue(value, fieldType) {
|
|
271
|
+
if (value === null || value === void 0) {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
switch (fieldType) {
|
|
275
|
+
case "datetime":
|
|
276
|
+
if (value instanceof Date) {
|
|
277
|
+
if (isNaN(value.getTime())) {
|
|
278
|
+
throw new Error(`Invalid Date object received from database`);
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
if (typeof value === "string") {
|
|
283
|
+
let date;
|
|
284
|
+
if (this.#dialect === "mysql") {
|
|
285
|
+
const normalized = value.includes("T") ? value : value.replace(" ", "T") + "Z";
|
|
286
|
+
date = new Date(normalized);
|
|
287
|
+
} else {
|
|
288
|
+
date = new Date(value);
|
|
289
|
+
}
|
|
290
|
+
if (isNaN(date.getTime())) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Invalid date value: "${value}" cannot be parsed as a valid date`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return date;
|
|
296
|
+
}
|
|
297
|
+
return value;
|
|
298
|
+
case "boolean":
|
|
299
|
+
if (this.#dialect === "postgresql") {
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
if (typeof value === "number") {
|
|
303
|
+
return value !== 0;
|
|
304
|
+
}
|
|
305
|
+
if (typeof value === "string") {
|
|
306
|
+
return value !== "0" && value !== "";
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
case "json":
|
|
310
|
+
if (typeof value === "string") {
|
|
311
|
+
return JSON.parse(value);
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
default:
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
230
318
|
async transaction(fn) {
|
|
231
319
|
const dialect = this.#dialect;
|
|
232
320
|
const handleError = this.#handleError.bind(this);
|
|
233
321
|
const supportsReturning = this.supportsReturning;
|
|
322
|
+
const encodeValue = this.encodeValue.bind(this);
|
|
323
|
+
const decodeValue = this.decodeValue.bind(this);
|
|
234
324
|
return await this.#sql.transaction(async (txSql) => {
|
|
235
325
|
const txDriver = {
|
|
236
326
|
supportsReturning,
|
|
327
|
+
encodeValue,
|
|
328
|
+
decodeValue,
|
|
237
329
|
all: async (strings, values) => {
|
|
238
330
|
try {
|
|
239
331
|
const { sql, params } = buildSQL(strings, values, dialect);
|
|
@@ -359,6 +451,9 @@ var BunDriver = class {
|
|
|
359
451
|
step = 4;
|
|
360
452
|
await this.#checkMissingConstraints(table);
|
|
361
453
|
}
|
|
454
|
+
step = 5;
|
|
455
|
+
const viewApplied = await this.#ensureViews(table);
|
|
456
|
+
applied = applied || viewApplied;
|
|
362
457
|
return { applied };
|
|
363
458
|
} catch (error) {
|
|
364
459
|
if (error instanceof SchemaDriftError || error instanceof EnsureError) {
|
|
@@ -371,6 +466,19 @@ var BunDriver = class {
|
|
|
371
466
|
);
|
|
372
467
|
}
|
|
373
468
|
}
|
|
469
|
+
async ensureView(viewObj) {
|
|
470
|
+
await this.#ensureSqliteInit();
|
|
471
|
+
const ddlTemplate = generateViewDDL(viewObj, { dialect: this.#dialect });
|
|
472
|
+
const ddlSQL = renderDDL(
|
|
473
|
+
ddlTemplate[0],
|
|
474
|
+
ddlTemplate.slice(1),
|
|
475
|
+
this.#dialect
|
|
476
|
+
);
|
|
477
|
+
for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
|
|
478
|
+
await this.#sql.unsafe(stmt.trim(), []);
|
|
479
|
+
}
|
|
480
|
+
return { applied: true };
|
|
481
|
+
}
|
|
374
482
|
async ensureConstraints(table) {
|
|
375
483
|
await this.#ensureSqliteInit();
|
|
376
484
|
const tableName = table.name;
|
|
@@ -634,7 +742,7 @@ var BunDriver = class {
|
|
|
634
742
|
return applied;
|
|
635
743
|
}
|
|
636
744
|
async #addColumn(table, fieldName) {
|
|
637
|
-
const { generateColumnDDL } = await import("../ddl-
|
|
745
|
+
const { generateColumnDDL } = await import("../ddl-2A2UFUR3.js");
|
|
638
746
|
const zodType = table.schema.shape[fieldName];
|
|
639
747
|
const fieldMeta = table.meta.fields[fieldName] || {};
|
|
640
748
|
const colTemplate = generateColumnDDL(
|
|
@@ -673,6 +781,30 @@ var BunDriver = class {
|
|
|
673
781
|
}
|
|
674
782
|
return applied;
|
|
675
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* Ensure the active view exists for this table (if it has soft delete).
|
|
786
|
+
* Creates the view using generateViewDDL.
|
|
787
|
+
*/
|
|
788
|
+
async #ensureViews(table) {
|
|
789
|
+
const meta = getTableMeta(table);
|
|
790
|
+
if (meta.softDeleteField && !meta.activeView) {
|
|
791
|
+
void table.active;
|
|
792
|
+
}
|
|
793
|
+
const activeView = meta.activeView;
|
|
794
|
+
if (!activeView) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
const ddlTemplate = generateViewDDL(activeView, { dialect: this.#dialect });
|
|
798
|
+
const ddlSQL = renderDDL(
|
|
799
|
+
ddlTemplate[0],
|
|
800
|
+
ddlTemplate.slice(1),
|
|
801
|
+
this.#dialect
|
|
802
|
+
);
|
|
803
|
+
for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
|
|
804
|
+
await this.#sql.unsafe(stmt.trim(), []);
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
676
808
|
async #createIndex(tableName, indexName, columns, unique) {
|
|
677
809
|
const uniqueKw = unique ? "UNIQUE " : "";
|
|
678
810
|
const colList = columns.map((c) => quoteIdent(c, this.#dialect)).join(", ");
|