cogsbox-shape 0.5.205 → 0.5.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -41
- package/cogsbox-shape-db/dist/cloudflare-d1/d1-driver.d.ts +29 -0
- package/cogsbox-shape-db/dist/cloudflare-d1/d1-driver.js +100 -0
- package/cogsbox-shape-db/dist/cloudflare-d1/index.d.ts +1 -0
- package/cogsbox-shape-db/dist/cloudflare-d1/index.js +1 -0
- package/cogsbox-shape-state/dist/example/type-hover-example.js +2 -2
- package/dist/schema.d.ts +51 -50
- package/dist/schema.js +90 -83
- package/dist/vitest/fullSchema.test.js +249 -127
- package/dist/vitest/refineRuntime.test.js +57 -18
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -33,14 +33,14 @@ Traditional approaches require defining these layers separately, leading to type
|
|
|
33
33
|
Define a field by chaining methods. Each step is optional — use only what you need.
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
s.sqlite()/s.postgres()/s.mysql() → .
|
|
36
|
+
s.sqlite()/s.postgres()/s.mysql() → .client() → .clientCheck() → .server() → .transform()
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
| Method | Purpose |
|
|
40
40
|
| --------------------------------- | -------------------------------------------------------------- |
|
|
41
41
|
| `s.sqlite/postgres/mysql({ type, sqlOnly })` | Database column type. `sqlOnly` excludes from client layer. |
|
|
42
|
-
| `.
|
|
43
|
-
| `.
|
|
42
|
+
| `.client({ value, schema })` | Client-side input schema and default value for new records. |
|
|
43
|
+
| `.clientCheck(fn)` | Client-side validation on the final client union type. |
|
|
44
44
|
| `.server(fn)` | Server-side validation. Stricter rules before database writes. |
|
|
45
45
|
| `.transform({ toClient, toDb })` | Converts between database and client representations. |
|
|
46
46
|
|
|
@@ -88,47 +88,49 @@ s.mysql({ type: "enum", values: ["draft", "published"] });
|
|
|
88
88
|
// SQL: ENUM('draft', 'published')
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
### 2. Client
|
|
91
|
+
### 2. Client — Defaults and Client-Side Validation
|
|
92
92
|
|
|
93
|
-
`.
|
|
93
|
+
`.client()` sets the default value and client-side validation type for new records.
|
|
94
94
|
|
|
95
95
|
```typescript
|
|
96
96
|
const userSchema = schema({
|
|
97
97
|
_tableName: "users",
|
|
98
98
|
// DB stores auto-increment integers, but new records need a temp string ID
|
|
99
|
-
id: s.sqlite({ type: "int", pk: true }).
|
|
99
|
+
id: s.sqlite({ type: "int", pk: true }).client({
|
|
100
100
|
value: () => crypto.randomUUID(),
|
|
101
101
|
schema: z.string(),
|
|
102
102
|
}),
|
|
103
|
-
//
|
|
103
|
+
// client type: string (just the user's schema)
|
|
104
104
|
// Default value: a generated UUID string
|
|
105
105
|
|
|
106
106
|
// Simple default without type override
|
|
107
|
-
name: s.sqlite({ type: "varchar" }).
|
|
108
|
-
//
|
|
107
|
+
name: s.sqlite({ type: "varchar" }).client({ value: "Anonymous" }),
|
|
108
|
+
// client type: string (inherits from SQL)
|
|
109
109
|
// Default value: "Anonymous"
|
|
110
110
|
|
|
111
|
-
// Type-only override
|
|
112
|
-
count: s.sqlite({ type: "int" }).
|
|
113
|
-
//
|
|
114
|
-
// Default value: inferred from
|
|
111
|
+
// Type-only override; default is inferred from the client schema
|
|
112
|
+
count: s.sqlite({ type: "int" }).client(() => z.number().min(0)),
|
|
113
|
+
// client type: number (with min validation)
|
|
114
|
+
// Default value: inferred from the client schema (0 for number)
|
|
115
115
|
});
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
**Note:** The final `client` schema is a union of `sql |
|
|
118
|
+
**Note:** The final `client` schema is a union of `sql | client` types, representing the complete app state after transforms.
|
|
119
|
+
|
|
120
|
+
`generateDefaults()` uses the client schema to pick a default when no explicit `value` is provided. `toClient()` stays for DB-shaped data.
|
|
119
121
|
|
|
120
|
-
### 3. Client — Client-Side Validation
|
|
122
|
+
### 3. Client Check — Client-Side Validation
|
|
121
123
|
|
|
122
|
-
`.
|
|
124
|
+
`.clientCheck()` adds validation rules to the final `client` schema (the union of sql | client). Use it for client-side validation that operates on the complete client type.
|
|
123
125
|
|
|
124
126
|
```typescript
|
|
125
127
|
name: s.sqlite({ type: "varchar" })
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
.server((tools) => tools.
|
|
128
|
+
.client({ value: "" })
|
|
129
|
+
.clientCheck((tools) => tools.client.min(3, "Too short"))
|
|
130
|
+
.server((tools) => tools.client.min(5, "Must be at least 5 chars")),
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
The `.
|
|
133
|
+
The `.clientCheck()` callback receives `tools` with `sql`, `client`, and `clientCheck` schemas.
|
|
132
134
|
|
|
133
135
|
### 4. Server — Server-Side Validation
|
|
134
136
|
|
|
@@ -152,8 +154,8 @@ The callback receives the previous schema in the chain so you can refine it:
|
|
|
152
154
|
```typescript
|
|
153
155
|
name: s
|
|
154
156
|
.sqlite({ type: "varchar" })
|
|
155
|
-
.
|
|
156
|
-
.server(({
|
|
157
|
+
.client(() => z.string().trim())
|
|
158
|
+
.server(({ client }) => client.min(2, "Too short")),
|
|
157
159
|
```
|
|
158
160
|
|
|
159
161
|
### 5. Transform — Convert Between Layers
|
|
@@ -163,7 +165,7 @@ name: s
|
|
|
163
165
|
```typescript
|
|
164
166
|
status: s
|
|
165
167
|
.sqlite({ type: "int" }) // DB: 0 or 1
|
|
166
|
-
.
|
|
168
|
+
.client(() => z.enum(["active", "inactive"])) // Client input: string enum
|
|
167
169
|
.transform({
|
|
168
170
|
toClient: (dbValue) => dbValue === 1 ? "active" : "inactive",
|
|
169
171
|
toDb: (clientValue) => clientValue === "active" ? 1 : 0,
|
|
@@ -193,13 +195,13 @@ const userSchema = schema({
|
|
|
193
195
|
|
|
194
196
|
#### Client-Only Fields
|
|
195
197
|
|
|
196
|
-
By skipping `s.sqlite()` entirely and just using `s.
|
|
198
|
+
By skipping `s.sqlite()` entirely and just using `s.client()`, you can define fields that exist purely on the client (like a temporary UI state or computed field) and will not be sent to the database.
|
|
197
199
|
|
|
198
200
|
```typescript
|
|
199
201
|
const products = schema({
|
|
200
202
|
_tableName: "products",
|
|
201
203
|
price: s.sqlite({ type: "int" }),
|
|
202
|
-
formattedPrice: s.
|
|
204
|
+
formattedPrice: s.client(""), // Client-only field!
|
|
203
205
|
});
|
|
204
206
|
```
|
|
205
207
|
|
|
@@ -213,11 +215,11 @@ const products = schema({
|
|
|
213
215
|
```typescript
|
|
214
216
|
const users = schema({
|
|
215
217
|
_tableName: "users",
|
|
216
|
-
firstName: s.sqlite({ type: "varchar" }).
|
|
217
|
-
lastName: s.sqlite({ type: "varchar" }).
|
|
218
|
+
firstName: s.sqlite({ type: "varchar" }).client({ value: "John" }),
|
|
219
|
+
lastName: s.sqlite({ type: "varchar" }).client({ value: "Doe" }),
|
|
218
220
|
|
|
219
|
-
// Virtual field. It exists in app/view state, not SQL.
|
|
220
|
-
fullName: s.
|
|
221
|
+
// Virtual field. It exists in app/view state, not SQL.
|
|
222
|
+
fullName: s.client(""),
|
|
221
223
|
|
|
222
224
|
// Hidden DB column. It is written to SQL, but not sent to the client.
|
|
223
225
|
searchIndex: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
@@ -235,19 +237,19 @@ During partial ORM updates, DB-backed derivations fetch only missing dependency
|
|
|
235
237
|
|
|
236
238
|
### 7. Refinement (`.refine()`)
|
|
237
239
|
|
|
238
|
-
`.refine()` adds cross-field validation rules that the entire row must satisfy. Unlike `.
|
|
240
|
+
`.refine()` adds cross-field validation rules that the entire row must satisfy. Unlike `.clientCheck()`/`.server()` which validate individual fields, `refine` can check relationships between fields.
|
|
239
241
|
|
|
240
242
|
```typescript
|
|
241
243
|
const events = schema({
|
|
242
244
|
_tableName: "events",
|
|
243
245
|
id: s.sqlite({ type: "int", pk: true }),
|
|
244
|
-
startDate: s.sqlite({ type: "varchar" }).
|
|
245
|
-
endDate: s.sqlite({ type: "varchar" }).
|
|
246
|
-
content: s.sqlite({ type: "varchar", nullable: true }).
|
|
246
|
+
startDate: s.sqlite({ type: "varchar" }).client({ value: "" }),
|
|
247
|
+
endDate: s.sqlite({ type: "varchar" }).client({ value: "" }),
|
|
248
|
+
content: s.sqlite({ type: "varchar", nullable: true }).client({
|
|
247
249
|
value: null,
|
|
248
250
|
schema: z.string().nullable(),
|
|
249
251
|
}),
|
|
250
|
-
isPublished: s.sqlite({ type: "boolean" }).
|
|
252
|
+
isPublished: s.sqlite({ type: "boolean" }).client({ value: false }),
|
|
251
253
|
}).refine((r) => [
|
|
252
254
|
r("server", (row) => {
|
|
253
255
|
const errors: { path: string[]; message: string }[] = [];
|
|
@@ -276,8 +278,8 @@ The `refine()` method takes a callback that receives an `r` helper function. Eac
|
|
|
276
278
|
| Layer | Applies to | Purpose |
|
|
277
279
|
|-------|-----------|---------|
|
|
278
280
|
| `"server"` | `parseForDb()`, `server` schema | Cross-field validation before DB writes |
|
|
279
|
-
| `"
|
|
280
|
-
| `"
|
|
281
|
+
| `"clientCheck"` | `clientCheck` schema | Cross-field validation on client output |
|
|
282
|
+
| `"client"` | `client` schema | Cross-field validation on raw client input |
|
|
281
283
|
| `"sql"` | `parseFromDb()`, `sql` schema | Cross-field validation on DB reads |
|
|
282
284
|
| `"all"` | all of the above | Universal cross-field validation |
|
|
283
285
|
| `string[]` | specified layers | Apply to multiple layers at once |
|
|
@@ -312,10 +314,10 @@ The returned schema object has a clear separation of concerns:
|
|
|
312
314
|
```typescript
|
|
313
315
|
const schema = createSchema(mySchema);
|
|
314
316
|
|
|
315
|
-
schema.schemas; // { sql,
|
|
317
|
+
schema.schemas; // { sql, client, clientChecked, server } — Zod schemas
|
|
316
318
|
schema.transforms; // { toClient, toDb, parseForDb, parseFromDb } — transformations
|
|
317
319
|
schema.defaults; // Default values for forms
|
|
318
|
-
schema.generateDefaults; // Function to generate fresh defaults (executes randomizers)
|
|
320
|
+
schema.generateDefaults; // Function to generate fresh client defaults (executes randomizers)
|
|
319
321
|
schema.pk; // Primary key field names
|
|
320
322
|
schema.clientPk; // Client-side primary key field names
|
|
321
323
|
schema.isClientRecord; // Function to check if a record is client-created
|
|
@@ -334,7 +336,7 @@ import { s, schema, createSchema } from "cogsbox-shape";
|
|
|
334
336
|
|
|
335
337
|
const contactSchema = schema({
|
|
336
338
|
_tableName: "contacts",
|
|
337
|
-
id: s.sqlite({ type: "int", pk: true }).
|
|
339
|
+
id: s.sqlite({ type: "int", pk: true }).client({
|
|
338
340
|
value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
|
|
339
341
|
schema: z.string(),
|
|
340
342
|
}),
|
|
@@ -342,7 +344,7 @@ const contactSchema = schema({
|
|
|
342
344
|
email: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.email()),
|
|
343
345
|
isActive: s
|
|
344
346
|
.sqlite({ type: "boolean", default: true })
|
|
345
|
-
.
|
|
347
|
+
.client(() => z.boolean())
|
|
346
348
|
.transform({
|
|
347
349
|
toClient: (val) => Boolean(val),
|
|
348
350
|
toDb: (val) => (val ? 1 : 0),
|
|
@@ -352,7 +354,7 @@ const contactSchema = schema({
|
|
|
352
354
|
const schema = createSchema(contactSchema);
|
|
353
355
|
|
|
354
356
|
// Access schemas directly
|
|
355
|
-
const { sql,
|
|
357
|
+
const { sql, client, clientChecked, server } = schema.schemas;
|
|
356
358
|
const { defaults, generateDefaults } = schema;
|
|
357
359
|
|
|
358
360
|
// Transforms for converting between layers
|
|
@@ -467,6 +469,22 @@ const user = await userView.findById(1);
|
|
|
467
469
|
// user.posts is loaded and validated as part of the view shape
|
|
468
470
|
```
|
|
469
471
|
|
|
472
|
+
Cloudflare D1 uses the same SQLite schema dialect with a D1 connection helper:
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import { connect } from "cogsbox-shape/db";
|
|
476
|
+
import { createD1Db } from "cogsbox-shape/db/cloudflare-d1";
|
|
477
|
+
|
|
478
|
+
export default {
|
|
479
|
+
async fetch(_request, env) {
|
|
480
|
+
const db = createD1Db(env.DB);
|
|
481
|
+
const bx = connect(box, db);
|
|
482
|
+
|
|
483
|
+
return Response.json(await bx.users.findMany());
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
```
|
|
487
|
+
|
|
470
488
|
Use `insert(data).ids()` when you only need the database identity, or `insert(data).full()` when you want optimistic client IDs reconciled back into the submitted client object. `create()` is kept as an alias for older code; prefer `insert()` in new code.
|
|
471
489
|
|
|
472
490
|
### 5. Nested Defaults and Form Definitions (`defaultsDefinition`)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import type { DatabaseIntrospector, Dialect, DialectAdapter, Driver, QueryCompiler } from "kysely";
|
|
3
|
+
export interface D1Result<T = Record<string, unknown>> {
|
|
4
|
+
success: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
results?: T[];
|
|
7
|
+
meta?: {
|
|
8
|
+
changes?: number;
|
|
9
|
+
last_row_id?: number;
|
|
10
|
+
rows_written?: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface D1PreparedStatement {
|
|
14
|
+
bind(...values: unknown[]): D1PreparedStatement;
|
|
15
|
+
all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
16
|
+
run<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
17
|
+
}
|
|
18
|
+
export interface D1Database {
|
|
19
|
+
prepare(query: string): D1PreparedStatement;
|
|
20
|
+
}
|
|
21
|
+
export declare class D1Dialect implements Dialect {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(database: D1Database);
|
|
24
|
+
createDriver(): Driver;
|
|
25
|
+
createQueryCompiler(): QueryCompiler;
|
|
26
|
+
createAdapter(): DialectAdapter;
|
|
27
|
+
createIntrospector(db: Kysely<any>): DatabaseIntrospector;
|
|
28
|
+
}
|
|
29
|
+
export declare function createD1Db<TDb = unknown>(database: D1Database): Kysely<TDb>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CompiledQuery, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from "kysely";
|
|
2
|
+
export class D1Dialect {
|
|
3
|
+
#database;
|
|
4
|
+
constructor(database) {
|
|
5
|
+
this.#database = database;
|
|
6
|
+
}
|
|
7
|
+
createDriver() {
|
|
8
|
+
return new D1Driver(this.#database);
|
|
9
|
+
}
|
|
10
|
+
createQueryCompiler() {
|
|
11
|
+
return new SqliteQueryCompiler();
|
|
12
|
+
}
|
|
13
|
+
createAdapter() {
|
|
14
|
+
return new SqliteAdapter();
|
|
15
|
+
}
|
|
16
|
+
createIntrospector(db) {
|
|
17
|
+
return new SqliteIntrospector(db);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function createD1Db(database) {
|
|
21
|
+
return new Kysely({
|
|
22
|
+
dialect: new D1Dialect(database),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
class D1Driver {
|
|
26
|
+
#connection;
|
|
27
|
+
constructor(database) {
|
|
28
|
+
this.#connection = new D1Connection(database);
|
|
29
|
+
}
|
|
30
|
+
async init(_options) {
|
|
31
|
+
// Cloudflare D1 bindings are already initialized by the Worker runtime.
|
|
32
|
+
}
|
|
33
|
+
async acquireConnection(_options) {
|
|
34
|
+
return this.#connection;
|
|
35
|
+
}
|
|
36
|
+
async beginTransaction(connection, _settings) {
|
|
37
|
+
await connection.executeQuery(CompiledQuery.raw("begin"));
|
|
38
|
+
}
|
|
39
|
+
async commitTransaction(connection) {
|
|
40
|
+
await connection.executeQuery(CompiledQuery.raw("commit"));
|
|
41
|
+
}
|
|
42
|
+
async rollbackTransaction(connection) {
|
|
43
|
+
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
|
44
|
+
}
|
|
45
|
+
async releaseConnection(_connection, _options) {
|
|
46
|
+
// D1 exposes a stateless binding instead of pooled connections.
|
|
47
|
+
}
|
|
48
|
+
async destroy(_options) {
|
|
49
|
+
// Nothing to close for a Worker binding.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
class D1Connection {
|
|
53
|
+
#database;
|
|
54
|
+
constructor(database) {
|
|
55
|
+
this.#database = database;
|
|
56
|
+
}
|
|
57
|
+
async executeQuery(compiledQuery) {
|
|
58
|
+
const statement = this.#prepare(compiledQuery);
|
|
59
|
+
const result = shouldReturnRows(compiledQuery.sql)
|
|
60
|
+
? await statement.all()
|
|
61
|
+
: await statement.run();
|
|
62
|
+
assertD1Success(result, compiledQuery.sql);
|
|
63
|
+
return {
|
|
64
|
+
insertId: result.meta?.last_row_id != null
|
|
65
|
+
? BigInt(result.meta.last_row_id)
|
|
66
|
+
: undefined,
|
|
67
|
+
numAffectedRows: affectedRows(result),
|
|
68
|
+
rows: (result.results ?? []),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async *streamQuery(compiledQuery, _chunkSize) {
|
|
72
|
+
const result = await this.executeQuery(compiledQuery);
|
|
73
|
+
for (const row of result.rows) {
|
|
74
|
+
yield { rows: [row] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
#prepare(compiledQuery) {
|
|
78
|
+
const statement = this.#database.prepare(compiledQuery.sql);
|
|
79
|
+
return compiledQuery.parameters.length > 0
|
|
80
|
+
? statement.bind(...compiledQuery.parameters)
|
|
81
|
+
: statement;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function shouldReturnRows(sql) {
|
|
85
|
+
const trimmed = sql.trim().toLowerCase();
|
|
86
|
+
return (trimmed.startsWith("select") ||
|
|
87
|
+
trimmed.startsWith("with") ||
|
|
88
|
+
trimmed.startsWith("pragma") ||
|
|
89
|
+
trimmed.startsWith("explain") ||
|
|
90
|
+
/\breturning\b/i.test(sql));
|
|
91
|
+
}
|
|
92
|
+
function affectedRows(result) {
|
|
93
|
+
const count = result.meta?.changes ?? result.meta?.rows_written;
|
|
94
|
+
return count != null ? BigInt(count) : undefined;
|
|
95
|
+
}
|
|
96
|
+
function assertD1Success(result, sql) {
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
throw new Error(result.error ?? `D1 query failed: ${sql}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createD1Db, D1Dialect, type D1Database, type D1PreparedStatement, type D1Result, } from "./d1-driver.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createD1Db, D1Dialect, } from "./d1-driver.js";
|
|
@@ -13,11 +13,11 @@ const taskItemSchema = z.object({
|
|
|
13
13
|
});
|
|
14
14
|
const taskManagerSchema = schema({
|
|
15
15
|
_tableName: "client",
|
|
16
|
-
tasks: s.
|
|
16
|
+
tasks: s.client({
|
|
17
17
|
value: [],
|
|
18
18
|
schema: z.array(taskItemSchema),
|
|
19
19
|
}),
|
|
20
|
-
filter: s.
|
|
20
|
+
filter: s.client({
|
|
21
21
|
value: "all",
|
|
22
22
|
schema: z.string(),
|
|
23
23
|
}),
|