@b9g/zen 0.1.0 → 0.1.2
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 +40 -0
- package/README.md +178 -64
- package/{chunk-QXGEP5PB.js → chunk-ARUUB3H4.js} +27 -2
- package/{chunk-56M5Z3A6.js → chunk-BEX6VPES.js} +192 -4
- package/chunk-NBXBBEMA.js +63 -0
- package/ddl-OT6HPLQY.js +11 -0
- package/package.json +10 -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 +6 -7
- package/src/zen.js +193 -49
- package/chunk-2IEEEMRN.js +0 -38
- package/ddl-NAJM37GQ.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.2] - 2025-12-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- New type exports: `PartialTable`, `DerivedTable`, `SetValues`, `FieldDBMeta`, `ReferenceInfo`, `CompoundReference`, `TaggedQuery`, `SQLDialect`, `isTable`
|
|
13
|
+
- Views documentation section in README
|
|
14
|
+
- `EnsureError` and `SchemaDriftError` documented in error types
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Reorganized `zen.ts` exports into logical groups
|
|
19
|
+
- README Types section now accurately reflects actual exports (removed non-existent `SQLFragment`, `DDLFragment`, `DBExpression`)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `isTable` type guard now exported (was missing)
|
|
24
|
+
|
|
25
|
+
## [0.1.1] - 2025-12-21
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Driver-level type encoding/decoding for dialect-specific handling
|
|
30
|
+
- `encodeValue(value, fieldType)` and `decodeValue(value, fieldType)` methods on Driver interface
|
|
31
|
+
- SQLite: Date→ISO string, boolean→1/0, JSON stringify/parse
|
|
32
|
+
- MySQL: Date→"YYYY-MM-DD HH:MM:SS", boolean→1/0, JSON stringify/parse
|
|
33
|
+
- PostgreSQL: Mostly passthrough (pg handles natively), JSON stringify
|
|
34
|
+
- `inferFieldType()` helper to infer field type from Zod schema
|
|
35
|
+
- Node.js tests for encode/decode functionality
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Breaking:** Removed deprecated `Infer<T>` type alias (use `Row<T>` instead)
|
|
40
|
+
- Renamed internal types for clarity:
|
|
41
|
+
- `InferRefs` → `RowRefs`
|
|
42
|
+
- `WithRefs` → `JoinedRow`
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Invalid datetime values now throw errors instead of returning Invalid Date
|
|
47
|
+
|
|
8
48
|
## [0.1.0] - 2025-12-20
|
|
9
49
|
|
|
10
50
|
Initial release of @b9g/zen - the simple database client.
|
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# ZenDB
|
|
2
|
-
The simple database client.
|
|
3
2
|
|
|
4
|
-
Define tables. Write SQL. Get objects.
|
|
3
|
+
The simple database client. Define tables. Write SQL. Get objects.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
[Website](https://zendb.org) · [GitHub](https://github.com/bikeshaving/zen) · [npm](https://www.npmjs.com/package/@b9g/zen)
|
|
7
6
|
|
|
8
7
|
## Installation
|
|
9
8
|
|
|
@@ -58,7 +57,7 @@ const Posts = table("posts", {
|
|
|
58
57
|
id: z.string().uuid().db.primary().db.auto(),
|
|
59
58
|
authorId: z.string().uuid().db.references(Users, "author"),
|
|
60
59
|
title: z.string(),
|
|
61
|
-
published: z.boolean().
|
|
60
|
+
published: z.boolean().db.inserted(() => false),
|
|
62
61
|
});
|
|
63
62
|
|
|
64
63
|
// 2. Create database with migrations
|
|
@@ -111,6 +110,32 @@ const post = await db.get(Posts, posts[0].id);
|
|
|
111
110
|
await db.update(Users, {name: "Alice Smith"}, user.id);
|
|
112
111
|
```
|
|
113
112
|
|
|
113
|
+
## Why Zen?
|
|
114
|
+
|
|
115
|
+
Zen is the missing link between SQL and typed data. By writing tables with Zod schema, you get idempotent migration helpers, typed CRUD, normalized object references, and many features other database clients cannot provide.
|
|
116
|
+
|
|
117
|
+
### What Zen is not:
|
|
118
|
+
- **Zen is not a query builder** — Rather than using a fluent query builder interface (`.where().orderBy().limit()`), Zen uses explicit SQL tagged template functions instead
|
|
119
|
+
```
|
|
120
|
+
db.get(Posts)`
|
|
121
|
+
WHERE ${Posts.cols.published} = ${true}
|
|
122
|
+
ORDER BY ${Posts.cols.publishDate}
|
|
123
|
+
DESC LIMIT 20
|
|
124
|
+
```.
|
|
125
|
+
- **Zen is not an ORM** — Tables are not classes, they are Zod-powered singletons which provide schema-aware SQL-fragment helpers. These tables can be passed to CRUD helpers to validate writes, generate DDL, and normalize joined data into an object graph.
|
|
126
|
+
- **Zen is not a startup** — Zen is an open-source library, not a venture-backed SaaS. There will never be a managed “ZenDB” instance or a “Zen Studio.” The library is a thin wrapper around Zod and JavaScript SQL drivers, with a focus on runtime abstractions rather than complicated tooling.
|
|
127
|
+
|
|
128
|
+
### Safety
|
|
129
|
+
|
|
130
|
+
- **No lazy loading** — Related data comes from your JOINs
|
|
131
|
+
- **No ORM identity map** — Normalization is per-query, not session-wide
|
|
132
|
+
- **No down migrations** — Forward-only versioning (1 → 2 → 3)
|
|
133
|
+
- **No destructive helpers** — No `dropColumn()`, `dropTable()`, `renameColumn()`
|
|
134
|
+
- **No automatic migrations** — Schema changes are explicit in upgrade events
|
|
135
|
+
|
|
136
|
+
Migrations are **additive and idempotent** by design. Use `ensureColumn()`, `ensureIndex()`, `copyColumn()` for safe schema evolution. Breaking changes require multi-step migrations. Rollbacks are new forward migrations.
|
|
137
|
+
|
|
138
|
+
|
|
114
139
|
## Table Definitions
|
|
115
140
|
|
|
116
141
|
```typescript
|
|
@@ -121,7 +146,7 @@ const Users = table("users", {
|
|
|
121
146
|
id: z.string().uuid().db.primary().db.auto(),
|
|
122
147
|
email: z.string().email().db.unique(),
|
|
123
148
|
name: z.string().max(100),
|
|
124
|
-
role: z.enum(["user", "admin"]).
|
|
149
|
+
role: z.enum(["user", "admin"]).db.inserted(() => "user"),
|
|
125
150
|
createdAt: z.date().db.auto(),
|
|
126
151
|
});
|
|
127
152
|
|
|
@@ -130,10 +155,23 @@ const Posts = table("posts", {
|
|
|
130
155
|
title: z.string(),
|
|
131
156
|
content: z.string().optional(),
|
|
132
157
|
authorId: z.string().uuid().db.references(Users, "author", {onDelete: "cascade"}),
|
|
133
|
-
published: z.boolean().
|
|
158
|
+
published: z.boolean().db.inserted(() => false),
|
|
134
159
|
});
|
|
135
160
|
```
|
|
136
161
|
|
|
162
|
+
**Zod to Database Behavior:**
|
|
163
|
+
|
|
164
|
+
| Zod Method | Effect |
|
|
165
|
+
|------------|--------|
|
|
166
|
+
| `.optional()` | Column allows `NULL`; field omittable on insert |
|
|
167
|
+
| `.nullable()` | Column allows `NULL`; must explicitly pass `null` or value |
|
|
168
|
+
| `.string().max(n)` | `VARCHAR(n)` in DDL (if n ≤ 255) |
|
|
169
|
+
| `.string().uuid()` | Used by `.db.auto()` to generate UUIDs |
|
|
170
|
+
| `.number().int()` | `INTEGER` column type |
|
|
171
|
+
| `.date()` | `TIMESTAMPTZ` / `DATETIME` / `TEXT` depending on dialect |
|
|
172
|
+
| `.object()` / `.array()` | Stored as JSON, auto-encoded/decoded |
|
|
173
|
+
| `.default()` | **Throws error** — use `.db.inserted()` instead |
|
|
174
|
+
|
|
137
175
|
**The `.db` namespace:**
|
|
138
176
|
|
|
139
177
|
The `.db` property is available on all Zod types imported from `@b9g/zen`. It provides database-specific modifiers:
|
|
@@ -253,7 +291,14 @@ await db.delete(Users, userId);
|
|
|
253
291
|
const activeUsers = await db.all(Users)`
|
|
254
292
|
WHERE NOT ${Users.deleted()}
|
|
255
293
|
`;
|
|
256
|
-
|
|
294
|
+
|
|
295
|
+
// Or use the .active view (auto-generated, read-only)
|
|
296
|
+
const activeUsers = await db.all(Users.active)``;
|
|
297
|
+
|
|
298
|
+
// JOINs with .active automatically filter deleted rows
|
|
299
|
+
const posts = await db.all([Posts, Users.active])`
|
|
300
|
+
JOIN "users_active" ON ${Users.active.cols.id} = ${Posts.cols.authorId}
|
|
301
|
+
`;
|
|
257
302
|
```
|
|
258
303
|
|
|
259
304
|
**Compound indexes** via table options:
|
|
@@ -267,6 +312,28 @@ const Posts = table("posts", {
|
|
|
267
312
|
});
|
|
268
313
|
```
|
|
269
314
|
|
|
315
|
+
**Compound foreign keys** for composite primary keys:
|
|
316
|
+
```typescript
|
|
317
|
+
const OrderProducts = table("order_products", {
|
|
318
|
+
orderId: z.string().uuid(),
|
|
319
|
+
productId: z.string().uuid(),
|
|
320
|
+
// ... compound primary key
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const OrderItems = table("order_items", {
|
|
324
|
+
id: z.string().uuid().db.primary(),
|
|
325
|
+
orderId: z.string().uuid(),
|
|
326
|
+
productId: z.string().uuid(),
|
|
327
|
+
quantity: z.number(),
|
|
328
|
+
}, {
|
|
329
|
+
references: [{
|
|
330
|
+
fields: ["orderId", "productId"],
|
|
331
|
+
table: OrderProducts,
|
|
332
|
+
as: "orderProduct",
|
|
333
|
+
}],
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
270
337
|
**Derived properties** for client-side transformations:
|
|
271
338
|
```typescript
|
|
272
339
|
const Posts = table("posts", {
|
|
@@ -279,27 +346,33 @@ const Posts = table("posts", {
|
|
|
279
346
|
derive: {
|
|
280
347
|
// Pure functions only (no I/O, no side effects)
|
|
281
348
|
titleUpper: (post) => post.title.toUpperCase(),
|
|
282
|
-
|
|
349
|
+
// Traverse relationships (requires JOIN in query)
|
|
350
|
+
tags: (post) => post.postTags?.map(pt => pt.tag?.name) ?? [],
|
|
283
351
|
}
|
|
284
352
|
});
|
|
285
353
|
|
|
286
|
-
|
|
354
|
+
type Post = Row<typeof Posts>;
|
|
355
|
+
// Post includes: id, title, authorId, titleUpper, tags
|
|
356
|
+
|
|
357
|
+
const posts = await db.all([Posts, Users, PostTags, Tags])`
|
|
287
358
|
JOIN "users" ON ${Users.on(Posts)}
|
|
359
|
+
LEFT JOIN "post_tags" ON ${PostTags.cols.postId} = ${Posts.cols.id}
|
|
360
|
+
LEFT JOIN "tags" ON ${Tags.on(PostTags)}
|
|
288
361
|
`;
|
|
289
362
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
post.
|
|
293
|
-
Object.keys(post); // ["id", "title", "authorId", "author"] (no
|
|
294
|
-
JSON.stringify(post); //
|
|
363
|
+
const post = posts[0];
|
|
364
|
+
post.titleUpper; // "HELLO WORLD" — typed as string
|
|
365
|
+
post.tags; // ["javascript", "typescript"] — traverses relationships
|
|
366
|
+
Object.keys(post); // ["id", "title", "authorId", "author"] (no derived props)
|
|
367
|
+
JSON.stringify(post); // Excludes derived properties (non-enumerable)
|
|
295
368
|
```
|
|
296
369
|
|
|
297
370
|
Derived properties:
|
|
298
371
|
- Are lazy getters (computed on access, not stored)
|
|
299
372
|
- Are non-enumerable (hidden from `Object.keys()` and `JSON.stringify()`)
|
|
300
373
|
- Must be pure functions (no I/O, no database queries)
|
|
301
|
-
-
|
|
302
|
-
- Are
|
|
374
|
+
- Can traverse resolved relationships from the same query
|
|
375
|
+
- Are fully typed via `Row<T>` inference
|
|
303
376
|
|
|
304
377
|
**Partial selects** with `pick()`:
|
|
305
378
|
```typescript
|
|
@@ -312,6 +385,55 @@ const posts = await db.all([Posts, UserSummary])`
|
|
|
312
385
|
|
|
313
386
|
**Table identity**: A table definition is a singleton value which is passed to database methods for validation, normalization, schema management, and convenient CRUD operations. It is not a class.
|
|
314
387
|
|
|
388
|
+
## Views
|
|
389
|
+
|
|
390
|
+
Views are read-only projections of tables with predefined WHERE clauses:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import {z, table, view} from "@b9g/zen";
|
|
394
|
+
|
|
395
|
+
const Users = table("users", {
|
|
396
|
+
id: z.string().db.primary(),
|
|
397
|
+
name: z.string(),
|
|
398
|
+
role: z.enum(["user", "admin"]),
|
|
399
|
+
deletedAt: z.date().nullable().db.softDelete(),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Define views with explicit names
|
|
403
|
+
const ActiveUsers = view("active_users", Users)`
|
|
404
|
+
WHERE ${Users.cols.deletedAt} IS NULL
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
const AdminUsers = view("admin_users", Users)`
|
|
408
|
+
WHERE ${Users.cols.role} = ${"admin"}
|
|
409
|
+
`;
|
|
410
|
+
|
|
411
|
+
// Query from views (same API as tables)
|
|
412
|
+
const admins = await db.all(AdminUsers)``;
|
|
413
|
+
const admin = await db.get(AdminUsers, "u1");
|
|
414
|
+
|
|
415
|
+
// Views are read-only — mutations throw errors
|
|
416
|
+
await db.insert(AdminUsers, {...}); // ✗ Error
|
|
417
|
+
await db.update(AdminUsers, {...}); // ✗ Error
|
|
418
|
+
await db.delete(AdminUsers, "u1"); // ✗ Error
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Auto-generated `.active` view:** Tables with a `.db.softDelete()` field automatically get an `.active` view:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// Equivalent to: view("users_active", Users)`WHERE deletedAt IS NULL`
|
|
425
|
+
const activeUsers = await db.all(Users.active)``;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Views preserve table relationships:** Views inherit references from their base table, so JOINs work identically:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
const posts = await db.all([Posts, AdminUsers])`
|
|
432
|
+
JOIN "admin_users" ON ${AdminUsers.on(Posts)}
|
|
433
|
+
`;
|
|
434
|
+
posts[0].author?.role; // "admin"
|
|
435
|
+
```
|
|
436
|
+
|
|
315
437
|
## Queries
|
|
316
438
|
|
|
317
439
|
Tagged templates with automatic parameterization:
|
|
@@ -485,7 +607,7 @@ zen provides idempotent helpers that encourage safe, additive-only migrations:
|
|
|
485
607
|
const Posts = table("posts", {
|
|
486
608
|
id: z.string().db.primary(),
|
|
487
609
|
title: z.string(),
|
|
488
|
-
views: z.number().
|
|
610
|
+
views: z.number().db.inserted(() => 0), // NEW - add to schema
|
|
489
611
|
});
|
|
490
612
|
|
|
491
613
|
if (e.oldVersion < 2) {
|
|
@@ -727,6 +849,8 @@ interface Driver {
|
|
|
727
849
|
|
|
728
850
|
**Migration locking**: If the driver provides `withMigrationLock()`, migrations run atomically (PostgreSQL uses advisory locks, MySQL uses `GET_LOCK`, SQLite uses exclusive transactions).
|
|
729
851
|
|
|
852
|
+
**Connection pooling**: Handled by the underlying driver. `postgres.js` and `mysql2` pool automatically; `better-sqlite3` uses a single connection (SQLite is single-writer anyway).
|
|
853
|
+
|
|
730
854
|
## Error Handling
|
|
731
855
|
|
|
732
856
|
All errors extend `DatabaseError` with typed error codes:
|
|
@@ -776,6 +900,8 @@ await db.transaction(async (tx) => {
|
|
|
776
900
|
- `AlreadyExistsError` — Unique constraint violated (tableName, field, value)
|
|
777
901
|
- `QueryError` — SQL execution failed (sql)
|
|
778
902
|
- `MigrationError` / `MigrationLockError` — Migration failures (fromVersion, toVersion)
|
|
903
|
+
- `EnsureError` — Schema ensure operation failed (operation, table, step)
|
|
904
|
+
- `SchemaDriftError` — Existing schema doesn't match definition (table, drift)
|
|
779
905
|
- `ConnectionError` / `TransactionError` — Connection/transaction issues
|
|
780
906
|
|
|
781
907
|
## Debugging
|
|
@@ -815,17 +941,27 @@ console.log(Posts.ddl().toString());
|
|
|
815
941
|
|
|
816
942
|
| Feature | SQLite | PostgreSQL | MySQL |
|
|
817
943
|
|---------|--------|------------|-------|
|
|
818
|
-
|
|
|
819
|
-
|
|
|
820
|
-
|
|
|
821
|
-
|
|
|
822
|
-
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
|
827
|
-
|
|
828
|
-
|
|
|
944
|
+
| RETURNING | ✅ | ✅ | ⚠️ fallback |
|
|
945
|
+
| IF NOT EXISTS (CREATE TABLE) | ✅ | ✅ | ✅ |
|
|
946
|
+
| IF NOT EXISTS (ADD COLUMN) | ✅ | ✅ | ⚠️ may error |
|
|
947
|
+
| Migration Locks | BEGIN EXCLUSIVE | pg_advisory_lock | GET_LOCK |
|
|
948
|
+
| Advisory Locks | — | ✅ | ✅ |
|
|
949
|
+
|
|
950
|
+
### Zod to SQL Type Mapping
|
|
951
|
+
|
|
952
|
+
| Zod Type | SQLite | PostgreSQL | MySQL |
|
|
953
|
+
|----------|--------|------------|-------|
|
|
954
|
+
| `z.string()` | TEXT | TEXT | TEXT |
|
|
955
|
+
| `z.string().max(n)` (n ≤ 255) | TEXT | VARCHAR(n) | VARCHAR(n) |
|
|
956
|
+
| `z.number()` | REAL | DOUBLE PRECISION | REAL |
|
|
957
|
+
| `z.number().int()` | INTEGER | INTEGER | INTEGER |
|
|
958
|
+
| `z.boolean()` | INTEGER | BOOLEAN | BOOLEAN |
|
|
959
|
+
| `z.date()` | TEXT | TIMESTAMPTZ | DATETIME |
|
|
960
|
+
| `z.enum([...])` | TEXT | TEXT | TEXT |
|
|
961
|
+
| `z.object({...})` | TEXT | JSONB | TEXT |
|
|
962
|
+
| `z.array(...)` | TEXT | JSONB | TEXT |
|
|
963
|
+
|
|
964
|
+
Override with `.db.type("CUSTOM")` when using custom encode/decode.
|
|
829
965
|
|
|
830
966
|
## Public API Reference
|
|
831
967
|
|
|
@@ -836,9 +972,11 @@ import {
|
|
|
836
972
|
// Zod (extended with .db namespace)
|
|
837
973
|
z, // Re-exported Zod with .db already available
|
|
838
974
|
|
|
839
|
-
// Table definition
|
|
975
|
+
// Table and view definition
|
|
840
976
|
table, // Create a table definition from Zod schema
|
|
977
|
+
view, // Create a read-only view from a table
|
|
841
978
|
isTable, // Type guard for Table objects
|
|
979
|
+
isView, // Type guard for View objects
|
|
842
980
|
extendZod, // Extend a separate Zod instance (advanced)
|
|
843
981
|
|
|
844
982
|
// Database
|
|
@@ -846,13 +984,12 @@ import {
|
|
|
846
984
|
Transaction, // Transaction context (passed to transaction callbacks)
|
|
847
985
|
DatabaseUpgradeEvent, // Event object for "upgradeneeded" handler
|
|
848
986
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
//
|
|
854
|
-
|
|
855
|
-
getDBMeta, // Get database metadata from a Zod schema
|
|
987
|
+
// SQL builtins (for .db.inserted() / .db.updated())
|
|
988
|
+
NOW, // CURRENT_TIMESTAMP alias
|
|
989
|
+
TODAY, // CURRENT_DATE alias
|
|
990
|
+
CURRENT_TIMESTAMP, // SQL CURRENT_TIMESTAMP
|
|
991
|
+
CURRENT_DATE, // SQL CURRENT_DATE
|
|
992
|
+
CURRENT_TIME, // SQL CURRENT_TIME
|
|
856
993
|
|
|
857
994
|
// Errors
|
|
858
995
|
DatabaseError, // Base error class
|
|
@@ -895,19 +1032,15 @@ import type {
|
|
|
895
1032
|
|
|
896
1033
|
// Fragment types
|
|
897
1034
|
SetValues, // Values accepted by Table.set()
|
|
898
|
-
|
|
899
|
-
DDLFragment, // DDL fragment object
|
|
1035
|
+
SQLTemplate, // SQL template object (return type of set(), on(), etc.)
|
|
900
1036
|
SQLDialect, // "sqlite" | "postgresql" | "mysql"
|
|
901
1037
|
|
|
902
1038
|
// Driver types
|
|
903
1039
|
Driver, // Driver interface for adapters
|
|
904
1040
|
TaggedQuery, // Tagged template query function
|
|
905
1041
|
|
|
906
|
-
// Expression types
|
|
907
|
-
DBExpression, // Runtime database expression
|
|
908
|
-
|
|
909
1042
|
// Error types
|
|
910
|
-
DatabaseErrorCode,
|
|
1043
|
+
DatabaseErrorCode, // Error code string literals
|
|
911
1044
|
} from "@b9g/zen";
|
|
912
1045
|
```
|
|
913
1046
|
|
|
@@ -961,6 +1094,9 @@ Users.pick("id", "email"); // PartialTable with subset of fields
|
|
|
961
1094
|
Users.derive("hasEmail", z.boolean())`
|
|
962
1095
|
${Users.cols.email} IS NOT NULL
|
|
963
1096
|
`;
|
|
1097
|
+
|
|
1098
|
+
// Views
|
|
1099
|
+
Users.active; // View excluding soft-deleted rows (read-only)
|
|
964
1100
|
```
|
|
965
1101
|
|
|
966
1102
|
### Database Methods
|
|
@@ -1020,25 +1156,3 @@ import PostgresDriver from "@b9g/zen/postgres";
|
|
|
1020
1156
|
// MySQL (mysql2)
|
|
1021
1157
|
import MySQLDriver from "@b9g/zen/mysql";
|
|
1022
1158
|
```
|
|
1023
|
-
|
|
1024
|
-
## What This Library Does Not Do
|
|
1025
|
-
|
|
1026
|
-
**Query Generation:**
|
|
1027
|
-
- **No model classes** — Tables are plain definitions, not class instances
|
|
1028
|
-
- **No hidden JOINs** — You write all SQL explicitly
|
|
1029
|
-
- **No implicit query building** — No `.where().orderBy().limit()` chains
|
|
1030
|
-
- **No lazy loading** — Related data comes from your JOINs
|
|
1031
|
-
- **No ORM identity map** — Normalization is per-query, not session-wide
|
|
1032
|
-
|
|
1033
|
-
**Migrations:**
|
|
1034
|
-
- **No down migrations** — Forward-only, monotonic versioning (1 → 2 → 3)
|
|
1035
|
-
- **No destructive helpers** — No `dropColumn()`, `dropTable()`, `renameColumn()` methods
|
|
1036
|
-
- **No automatic migrations** — DDL must be written explicitly in upgrade events
|
|
1037
|
-
- **No migration files** — Event handlers replace traditional migration folders
|
|
1038
|
-
- **No branching versions** — Linear version history only
|
|
1039
|
-
|
|
1040
|
-
**Safety Philosophy:**
|
|
1041
|
-
- Migrations are **additive and idempotent** by design
|
|
1042
|
-
- Use `ensureColumn()`, `ensureIndex()`, `copyColumn()` for safe schema changes
|
|
1043
|
-
- Breaking changes require multi-step migrations (add, migrate data, deprecate)
|
|
1044
|
-
- Version numbers never decrease — rollbacks are new forward migrations
|
|
@@ -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-BEX6VPES.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
|
};
|