cogsbox-shape 0.5.192 → 0.5.194
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 +126 -37
- package/cogsbox-shape-db/dist/connect.d.ts +39 -14
- package/cogsbox-shape-db/dist/connect.js +9 -5
- package/cogsbox-shape-db/dist/sqlite/sqlite-driver.d.ts +1 -1
- package/cogsbox-shape-db/dist/table-db.d.ts +10 -2
- package/cogsbox-shape-db/dist/table-db.js +7 -0
- package/cogsbox-shape-db/dist/types.d.ts +11 -6
- package/dist/generateSQL.js +148 -67
- package/dist/schema.d.ts +62 -8
- package/dist/schema.js +115 -42
- package/dist/vitest/fullSchema.test.d.ts +1 -0
- package/dist/vitest/fullSchema.test.js +1367 -0
- package/dist/vitest/generateSQL.test.d.ts +1 -0
- package/dist/vitest/generateSQL.test.js +70 -0
- package/dist/vitest/packageExports.test.d.ts +1 -0
- package/dist/vitest/packageExports.test.js +15 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -33,35 +33,60 @@ 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.
|
|
36
|
+
s.sqlite()/s.postgres()/s.mysql() → .clientInput() → .client() → .server() → .transform()
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
| Method | Purpose |
|
|
40
40
|
| --------------------------------- | -------------------------------------------------------------- |
|
|
41
|
-
| `s.
|
|
41
|
+
| `s.sqlite/postgres/mysql({ type, sqlOnly })` | Database column type. `sqlOnly` excludes from client layer. |
|
|
42
42
|
| `.clientInput({ value, schema })` | Client-side input schema and default value for new records. |
|
|
43
43
|
| `.client(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
|
|
|
47
|
-
Note: `.derive()`
|
|
47
|
+
Note: `.derive()` and `.refine()` are schema-level methods, not chainable on individual fields.
|
|
48
48
|
|
|
49
|
-
### 1. SQL — Define Your Database Schema
|
|
50
|
-
|
|
51
|
-
Start with your database reality:
|
|
49
|
+
### 1. SQL — Define Your Database Schema
|
|
50
|
+
|
|
51
|
+
Start with your database reality:
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
54
|
import { s, schema } from "cogsbox-shape";
|
|
55
55
|
|
|
56
56
|
const userSchema = schema({
|
|
57
57
|
_tableName: "users",
|
|
58
|
-
id: s.
|
|
59
|
-
email: s.
|
|
60
|
-
createdAt: s.
|
|
58
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
59
|
+
email: s.sqlite({ type: "varchar", length: 255 }),
|
|
60
|
+
createdAt: s.sqlite({ type: "datetime", default: "CURRENT_TIMESTAMP" }),
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
This generates a Zod schema matching your SQL types exactly.
|
|
64
|
+
This generates a Zod schema matching your SQL types exactly.
|
|
65
|
+
|
|
66
|
+
Use the SQL engine function that matches the database this schema targets:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
s.sqlite({ type: "text" });
|
|
70
|
+
s.postgres({ type: "varchar", length: 255 });
|
|
71
|
+
s.mysql({ type: "varchar", length: 255 });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Enums are real SQL column configs:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
s.sqlite({ type: "enum", values: ["draft", "published"] });
|
|
78
|
+
// SQL: TEXT CHECK (...)
|
|
79
|
+
|
|
80
|
+
s.postgres({
|
|
81
|
+
type: "enum",
|
|
82
|
+
name: "post_status",
|
|
83
|
+
values: ["draft", "published"],
|
|
84
|
+
});
|
|
85
|
+
// SQL: CREATE TYPE post_status AS ENUM (...), then column uses post_status
|
|
86
|
+
|
|
87
|
+
s.mysql({ type: "enum", values: ["draft", "published"] });
|
|
88
|
+
// SQL: ENUM('draft', 'published')
|
|
89
|
+
```
|
|
65
90
|
|
|
66
91
|
### 2. Client Input — Defaults and Client-Side Validation
|
|
67
92
|
|
|
@@ -71,7 +96,7 @@ This generates a Zod schema matching your SQL types exactly.
|
|
|
71
96
|
const userSchema = schema({
|
|
72
97
|
_tableName: "users",
|
|
73
98
|
// DB stores auto-increment integers, but new records need a temp string ID
|
|
74
|
-
id: s.
|
|
99
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
75
100
|
value: () => crypto.randomUUID(),
|
|
76
101
|
schema: z.string(),
|
|
77
102
|
}),
|
|
@@ -79,12 +104,12 @@ const userSchema = schema({
|
|
|
79
104
|
// Default value: a generated UUID string
|
|
80
105
|
|
|
81
106
|
// Simple default without type override
|
|
82
|
-
name: s.
|
|
107
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "Anonymous" }),
|
|
83
108
|
// clientInput type: string (inherits from SQL)
|
|
84
109
|
// Default value: "Anonymous"
|
|
85
110
|
|
|
86
111
|
// Type-only override (no default value change)
|
|
87
|
-
count: s.
|
|
112
|
+
count: s.sqlite({ type: "int" }).clientInput(() => z.number().min(0)),
|
|
88
113
|
// clientInput type: number (with min validation)
|
|
89
114
|
// Default value: inferred from type (0 for number)
|
|
90
115
|
});
|
|
@@ -97,7 +122,7 @@ const userSchema = schema({
|
|
|
97
122
|
`.client()` adds validation rules to the final `client` schema (the union of sql | clientInput). Use it for client-side validation that operates on the complete client type.
|
|
98
123
|
|
|
99
124
|
```typescript
|
|
100
|
-
name: s.
|
|
125
|
+
name: s.sqlite({ type: "varchar" })
|
|
101
126
|
.clientInput({ value: "" })
|
|
102
127
|
.client((tools) => tools.clientInput.min(3, "Too short"))
|
|
103
128
|
.server((tools) => tools.clientInput.min(5, "Must be at least 5 chars")),
|
|
@@ -113,11 +138,11 @@ The `.client()` callback receives `tools` with `sql`, `clientInput`, and `client
|
|
|
113
138
|
const userSchema = schema({
|
|
114
139
|
_tableName: "users",
|
|
115
140
|
email: s
|
|
116
|
-
.
|
|
141
|
+
.sqlite({ type: "varchar", length: 255 })
|
|
117
142
|
.server(({ sql }) => sql.email("Invalid email")),
|
|
118
143
|
|
|
119
144
|
age: s
|
|
120
|
-
.
|
|
145
|
+
.sqlite({ type: "int" })
|
|
121
146
|
.server(({ sql }) => sql.min(18, "Must be 18+").max(120)),
|
|
122
147
|
});
|
|
123
148
|
```
|
|
@@ -126,7 +151,7 @@ The callback receives the previous schema in the chain so you can refine it:
|
|
|
126
151
|
|
|
127
152
|
```typescript
|
|
128
153
|
name: s
|
|
129
|
-
.
|
|
154
|
+
.sqlite({ type: "varchar" })
|
|
130
155
|
.clientInput(() => z.string().trim())
|
|
131
156
|
.server(({ clientInput }) => clientInput.min(2, "Too short")),
|
|
132
157
|
```
|
|
@@ -137,7 +162,7 @@ name: s
|
|
|
137
162
|
|
|
138
163
|
```typescript
|
|
139
164
|
status: s
|
|
140
|
-
.
|
|
165
|
+
.sqlite({ type: "int" }) // DB: 0 or 1
|
|
141
166
|
.clientInput(() => z.enum(["active", "inactive"])) // Client input: string enum
|
|
142
167
|
.transform({
|
|
143
168
|
toClient: (dbValue) => dbValue === 1 ? "active" : "inactive",
|
|
@@ -158,9 +183,9 @@ Use `sqlOnly: true` to define fields that belong to the database exclusively (li
|
|
|
158
183
|
```typescript
|
|
159
184
|
const userSchema = schema({
|
|
160
185
|
_tableName: "users",
|
|
161
|
-
id: s.
|
|
162
|
-
email: s.
|
|
163
|
-
internalToken: s.
|
|
186
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
187
|
+
email: s.sqlite({ type: "varchar" }),
|
|
188
|
+
internalToken: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
164
189
|
});
|
|
165
190
|
// DB reads/writes: { id, email, internalToken }
|
|
166
191
|
// Client sees: { id, email }
|
|
@@ -168,12 +193,12 @@ const userSchema = schema({
|
|
|
168
193
|
|
|
169
194
|
#### Client-Only Fields
|
|
170
195
|
|
|
171
|
-
By skipping `s.
|
|
196
|
+
By skipping `s.sqlite()` entirely and just using `s.clientInput()`, 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.
|
|
172
197
|
|
|
173
198
|
```typescript
|
|
174
199
|
const products = schema({
|
|
175
200
|
_tableName: "products",
|
|
176
|
-
price: s.
|
|
201
|
+
price: s.sqlite({ type: "int" }),
|
|
177
202
|
formattedPrice: s.clientInput(""), // Client-only field!
|
|
178
203
|
});
|
|
179
204
|
```
|
|
@@ -188,14 +213,14 @@ const products = schema({
|
|
|
188
213
|
```typescript
|
|
189
214
|
const users = schema({
|
|
190
215
|
_tableName: "users",
|
|
191
|
-
firstName: s.
|
|
192
|
-
lastName: s.
|
|
216
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
217
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
|
|
193
218
|
|
|
194
219
|
// Virtual field. It exists in app/view state, not SQL.
|
|
195
220
|
fullName: s.clientInput(""),
|
|
196
221
|
|
|
197
222
|
// Hidden DB column. It is written to SQL, but not sent to the client.
|
|
198
|
-
searchIndex: s.
|
|
223
|
+
searchIndex: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
199
224
|
}).derive({
|
|
200
225
|
forClient: {
|
|
201
226
|
fullName: (row) => `${row.firstName} ${row.lastName}`,
|
|
@@ -206,9 +231,71 @@ const users = schema({
|
|
|
206
231
|
});
|
|
207
232
|
```
|
|
208
233
|
|
|
209
|
-
During partial ORM updates, DB-backed derivations fetch only missing dependency fields they actually read, then recompute the affected `forDb` fields. Client-only derived fields are ignored by SQL writes.
|
|
234
|
+
During partial ORM updates, DB-backed derivations fetch only missing dependency fields they actually read, then recompute the affected `forDb` fields. Client-only derived fields are ignored by SQL writes.
|
|
235
|
+
|
|
236
|
+
### 7. Refinement (`.refine()`)
|
|
237
|
+
|
|
238
|
+
`.refine()` adds cross-field validation rules that the entire row must satisfy. Unlike `.client()`/`.server()` which validate individual fields, `refine` can check relationships between fields.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const events = schema({
|
|
242
|
+
_tableName: "events",
|
|
243
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
244
|
+
startDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
245
|
+
endDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
246
|
+
content: s.sqlite({ type: "varchar", nullable: true }).clientInput({
|
|
247
|
+
value: null,
|
|
248
|
+
schema: z.string().nullable(),
|
|
249
|
+
}),
|
|
250
|
+
isPublished: s.sqlite({ type: "boolean" }).clientInput({ value: false }),
|
|
251
|
+
}).refine({
|
|
252
|
+
server: (row) => {
|
|
253
|
+
const errors: { path: string[]; message: string }[] = [];
|
|
254
|
+
if (row.startDate && row.endDate && row.startDate > row.endDate) {
|
|
255
|
+
errors.push({ path: ["endDate"], message: "End date must be after start date" });
|
|
256
|
+
}
|
|
257
|
+
if (row.isPublished && !row.content) {
|
|
258
|
+
errors.push({ path: ["content"], message: "Published events must have content" });
|
|
259
|
+
}
|
|
260
|
+
return errors.length > 0 ? errors : undefined;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const box = createSchemaBox({ events }, { events: {} });
|
|
265
|
+
|
|
266
|
+
// Server refinement runs on parseForDb (before DB writes)
|
|
267
|
+
box.events.transforms.parseForDb({
|
|
268
|
+
id: 1, startDate: "2024-12-31", endDate: "2024-01-01",
|
|
269
|
+
content: null, isPublished: false,
|
|
270
|
+
});
|
|
271
|
+
// Throws: "End date must be after start date"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The `refine()` config accepts two optional callbacks:
|
|
275
|
+
|
|
276
|
+
| Callback | Runs on | Purpose |
|
|
277
|
+
|----------|---------|---------|
|
|
278
|
+
| `server` | `parseForDb()` | Cross-field validation before DB writes |
|
|
279
|
+
| `client` | `clientInput` schema | Cross-field validation on client input |
|
|
280
|
+
|
|
281
|
+
Each callback receives the full row and returns:
|
|
282
|
+
- `undefined` or `null` — validation passes
|
|
283
|
+
- A single `{ path: string[]; message: string }` — one error
|
|
284
|
+
- An array of `{ path: string[]; message: string }` — multiple errors
|
|
285
|
+
|
|
286
|
+
**Dependency tracking**: Same proxy-based approach as `derive()` — the library tracks which fields the refine function reads. This is used by the ORM to know which fields to include during partial updates. The same caveat applies: conditional branches with falsy defaults can hide dependencies.
|
|
287
|
+
|
|
288
|
+
**Chaining**: `refine()` can be chained after `derive()`:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
schema({ ... })
|
|
292
|
+
.derive({ forDb: { fullName: (row) => `${row.firstName} ${row.lastName}` } })
|
|
293
|
+
.refine({ server: (row) => { ... } });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Note**: `parsePatchForDb` uses the base schema (without refinement) since partial data may not satisfy cross-field rules.
|
|
210
297
|
|
|
211
|
-
### Schema Object Structure
|
|
298
|
+
### Schema Object Structure
|
|
212
299
|
|
|
213
300
|
The returned schema object has a clear separation of concerns:
|
|
214
301
|
|
|
@@ -222,6 +309,8 @@ schema.generateDefaults; // Function to generate fresh defaults (executes random
|
|
|
222
309
|
schema.pk; // Primary key field names
|
|
223
310
|
schema.clientPk; // Client-side primary key field names
|
|
224
311
|
schema.isClientRecord; // Function to check if a record is client-created
|
|
312
|
+
schema.deriveDependencies; // Derive function dependencies ({ [field]: string[] })
|
|
313
|
+
schema.refineDependencies; // Refinement dependencies ({ server: string[], client: string[] })
|
|
225
314
|
```
|
|
226
315
|
|
|
227
316
|
## Using Schemas
|
|
@@ -235,14 +324,14 @@ import { s, schema, createSchema } from "cogsbox-shape";
|
|
|
235
324
|
|
|
236
325
|
const contactSchema = schema({
|
|
237
326
|
_tableName: "contacts",
|
|
238
|
-
id: s.
|
|
327
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
239
328
|
value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
|
|
240
329
|
schema: z.string(),
|
|
241
330
|
}),
|
|
242
|
-
name: s.
|
|
243
|
-
email: s.
|
|
331
|
+
name: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.min(2)),
|
|
332
|
+
email: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.email()),
|
|
244
333
|
isActive: s
|
|
245
|
-
.
|
|
334
|
+
.sqlite({ type: "boolean", default: true })
|
|
246
335
|
.clientInput(() => z.boolean())
|
|
247
336
|
.transform({
|
|
248
337
|
toClient: (val) => Boolean(val),
|
|
@@ -282,15 +371,15 @@ import { s, schema, createSchemaBox } from "cogsbox-shape";
|
|
|
282
371
|
|
|
283
372
|
const users = schema({
|
|
284
373
|
_tableName: "users",
|
|
285
|
-
id: s.
|
|
286
|
-
name: s.
|
|
374
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
375
|
+
name: s.sqlite({ type: "varchar" }),
|
|
287
376
|
posts: s.hasMany(), // Placeholder — resolved later
|
|
288
377
|
});
|
|
289
378
|
|
|
290
379
|
const posts = schema({
|
|
291
380
|
_tableName: "posts",
|
|
292
|
-
id: s.
|
|
293
|
-
title: s.
|
|
381
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
382
|
+
title: s.sqlite({ type: "varchar" }),
|
|
294
383
|
authorId: s.reference(() => users.id), // Foreign key
|
|
295
384
|
});
|
|
296
385
|
```
|
|
@@ -364,7 +453,7 @@ const userView = bx.users.createView({
|
|
|
364
453
|
posts: true,
|
|
365
454
|
});
|
|
366
455
|
|
|
367
|
-
const user = await userView.
|
|
456
|
+
const user = await userView.findById(1);
|
|
368
457
|
// user.posts is loaded and validated as part of the view shape
|
|
369
458
|
```
|
|
370
459
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
|
-
import {
|
|
2
|
+
import type { TableDBApi } from "./table-db.js";
|
|
3
3
|
type FirstArg<T> = T extends (arg: infer A, ...args: any[]) => any ? A : never;
|
|
4
4
|
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
|
|
5
|
+
type Row<T> = T extends readonly (infer TItem)[] ? TItem : T;
|
|
5
6
|
type Prettify<T> = {
|
|
6
7
|
[K in keyof T]: T[K];
|
|
7
8
|
} & {};
|
|
8
|
-
type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "primaryKeySQL" | "derive";
|
|
9
|
+
type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "__refinements" | "primaryKeySQL" | "derive" | "refine";
|
|
9
10
|
type SqlConfigOf<TField> = TField extends {
|
|
10
11
|
config: {
|
|
11
12
|
sql: infer TSql;
|
|
@@ -20,14 +21,14 @@ type SqlConfigBaseValue<TSql> = TSql extends {
|
|
|
20
21
|
} ? number : TSql extends {
|
|
21
22
|
type: "date" | "datetime" | "timestamp";
|
|
22
23
|
} ? Date : TSql extends {
|
|
23
|
-
type: "varchar" | "char" | "text" | "longtext";
|
|
24
|
+
type: "varchar" | "char" | "text" | "longtext" | "enum";
|
|
24
25
|
} ? string : unknown;
|
|
25
26
|
type SqlOnlyValue<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
|
|
26
27
|
nullable: true;
|
|
27
28
|
} ? SqlConfigBaseValue<TSql> | null : SqlConfigBaseValue<TSql> : unknown;
|
|
28
29
|
type IsSqlOnlyField<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
|
|
29
|
-
sqlOnly
|
|
30
|
-
} ? true
|
|
30
|
+
sqlOnly: true;
|
|
31
|
+
} ? true : false : false;
|
|
31
32
|
type IsOptionalSqlOnly<TField> = TField extends {
|
|
32
33
|
config: {
|
|
33
34
|
sql: {
|
|
@@ -47,27 +48,51 @@ type IsOptionalSqlOnly<TField> = TField extends {
|
|
|
47
48
|
};
|
|
48
49
|
};
|
|
49
50
|
} ? true : false;
|
|
51
|
+
type IsDerivedDbField<TTable, TKey> = TTable extends {
|
|
52
|
+
rawSchema: {
|
|
53
|
+
__derives?: {
|
|
54
|
+
forDb?: infer TForDb;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
} ? TKey extends keyof NonNullable<TForDb> ? true : false : false;
|
|
50
58
|
type SqlOnlyInput<T> = T extends {
|
|
51
59
|
definition: infer TDefinition;
|
|
52
60
|
} ? Prettify<{
|
|
53
|
-
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never :
|
|
61
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
|
|
62
|
+
__type: "reference";
|
|
63
|
+
} ? never : IsDerivedDbField<T, K> extends true ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? never : K : never]: SqlOnlyValue<TDefinition[K]>;
|
|
54
64
|
} & {
|
|
55
|
-
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never :
|
|
65
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
|
|
66
|
+
__type: "reference";
|
|
67
|
+
} ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? K : never : never]?: SqlOnlyValue<TDefinition[K]>;
|
|
56
68
|
}> : Record<string, never>;
|
|
69
|
+
type DbApiFor<T> = T extends {
|
|
70
|
+
transforms: {
|
|
71
|
+
parseForDb: (...args: any[]) => any;
|
|
72
|
+
parseFromDb: (...args: any[]) => any;
|
|
73
|
+
};
|
|
74
|
+
} ? TableDBApi<Row<Return<T["transforms"]["parseFromDb"]>>, Row<FirstArg<T["transforms"]["parseForDb"]>>, SqlOnlyInput<T>> : never;
|
|
75
|
+
type ConnectedView<T> = T extends {
|
|
76
|
+
transforms: {
|
|
77
|
+
parseForDb: (...args: any[]) => any;
|
|
78
|
+
parseFromDb: (...args: any[]) => any;
|
|
79
|
+
};
|
|
80
|
+
} ? Omit<T, keyof DbApiFor<T>> & DbApiFor<T> : T;
|
|
81
|
+
type ConnectedCreateView<T> = T extends {
|
|
82
|
+
createView: (...args: infer TArgs) => infer TView;
|
|
83
|
+
} ? {
|
|
84
|
+
createView: (...args: TArgs) => ConnectedView<TView>;
|
|
85
|
+
} : {};
|
|
57
86
|
type ConnectedTable<T> = T extends {
|
|
58
87
|
transforms: {
|
|
59
88
|
parseForDb: (...args: any[]) => any;
|
|
60
89
|
parseFromDb: (...args: any[]) => any;
|
|
61
90
|
};
|
|
62
|
-
} ? T &
|
|
63
|
-
db: TableDB<Return<T["transforms"]["parseFromDb"]>, FirstArg<T["transforms"]["parseForDb"]>, SqlOnlyInput<T>>;
|
|
64
|
-
} : T;
|
|
91
|
+
} ? Omit<T, "createView" | keyof DbApiFor<T>> & DbApiFor<T> & ConnectedCreateView<T> : T;
|
|
65
92
|
type ConnectedBox<T extends Record<string, unknown>> = {
|
|
66
93
|
[K in keyof T]: ConnectedTable<T[K]>;
|
|
67
94
|
} & {
|
|
68
|
-
|
|
69
|
-
transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
|
|
70
|
-
};
|
|
95
|
+
transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
|
|
71
96
|
};
|
|
72
|
-
export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<
|
|
97
|
+
export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<any>): ConnectedBox<T>;
|
|
73
98
|
export {};
|
|
@@ -91,8 +91,10 @@ function enhanceTable(entry, meta, db) {
|
|
|
91
91
|
});
|
|
92
92
|
return new Proxy(entry, {
|
|
93
93
|
get(target, prop, receiver) {
|
|
94
|
-
if (prop
|
|
95
|
-
|
|
94
|
+
if (prop in tableDb) {
|
|
95
|
+
const value = Reflect.get(tableDb, prop, tableDb);
|
|
96
|
+
return typeof value === "function" ? value.bind(tableDb) : value;
|
|
97
|
+
}
|
|
96
98
|
return Reflect.get(target, prop, receiver);
|
|
97
99
|
},
|
|
98
100
|
});
|
|
@@ -208,8 +210,10 @@ export function connect(box, db) {
|
|
|
208
210
|
}, reconcile, hydrateRow);
|
|
209
211
|
return new Proxy(view, {
|
|
210
212
|
get(target, prop, receiver) {
|
|
211
|
-
if (prop
|
|
212
|
-
|
|
213
|
+
if (prop in viewDb) {
|
|
214
|
+
const value = Reflect.get(viewDb, prop, viewDb);
|
|
215
|
+
return typeof value === "function" ? value.bind(viewDb) : value;
|
|
216
|
+
}
|
|
213
217
|
return Reflect.get(target, prop, receiver);
|
|
214
218
|
},
|
|
215
219
|
});
|
|
@@ -226,6 +230,6 @@ export function connect(box, db) {
|
|
|
226
230
|
return fn(txBox);
|
|
227
231
|
});
|
|
228
232
|
};
|
|
229
|
-
result.
|
|
233
|
+
result.transaction = transaction;
|
|
230
234
|
return result;
|
|
231
235
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
|
-
export declare function createSqliteDb(path: string): Promise<Kysely<
|
|
2
|
+
export declare function createSqliteDb<TDb = unknown>(path: string): Promise<Kysely<TDb>>;
|
|
@@ -5,13 +5,14 @@ type RequiredKeys<T> = {
|
|
|
5
5
|
[K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
|
|
6
6
|
}[keyof T];
|
|
7
7
|
type InsertDbOnlyArgs<T extends Record<string, unknown>> = keyof T extends never ? [] : RequiredKeys<T> extends never ? [dbOnlyData?: Partial<T>] : [dbOnlyData: T];
|
|
8
|
+
export type TableDBApi<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> = Pick<TableDB<TClient, TCreate, TDbOnly>, "findMany" | "findById" | "byId" | "insert" | "create" | "update" | "delete" | "count" | "reconcileIds">;
|
|
8
9
|
export declare class TableDB<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> {
|
|
9
10
|
private db;
|
|
10
11
|
private meta;
|
|
11
12
|
private transforms;
|
|
12
13
|
private reconcile?;
|
|
13
14
|
private hydrateRow?;
|
|
14
|
-
constructor(db: Kysely<
|
|
15
|
+
constructor(db: Kysely<any>, meta: TableMeta, transforms: {
|
|
15
16
|
toClient: (row: Record<string, unknown>) => TClient;
|
|
16
17
|
toDb: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
17
18
|
parseForDb: (data: Record<string, unknown>) => Record<string, unknown>;
|
|
@@ -22,6 +23,13 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
|
|
|
22
23
|
}) | undefined, hydrateRow?: ((row: Record<string, unknown>) => Promise<Record<string, unknown>>) | undefined);
|
|
23
24
|
findMany(opts?: FindManyOpts<TClient>): Promise<TClient[]>;
|
|
24
25
|
findById(id: unknown): Promise<TClient | null>;
|
|
26
|
+
byId(id: unknown): {
|
|
27
|
+
find: () => Promise<TClient | null>;
|
|
28
|
+
update: (data: Partial<TCreate>, dbOnlyData?: DbOnlyArg<TDbOnly>) => ReturnType<TableDB<TClient, TCreate, TDbOnly>["update"]>;
|
|
29
|
+
delete: () => Promise<{
|
|
30
|
+
deleted: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
25
33
|
insert(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): {
|
|
26
34
|
ids: () => Promise<Record<string, unknown>>;
|
|
27
35
|
full: () => Promise<TClient>;
|
|
@@ -39,7 +47,7 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
|
|
|
39
47
|
private pickDbPatchFields;
|
|
40
48
|
private isWritableDbColumn;
|
|
41
49
|
private parseDbOnlyData;
|
|
42
|
-
reconcileIds(clientData:
|
|
50
|
+
reconcileIds<TData>(clientData: TData, ids: unknown): TData;
|
|
43
51
|
private reconcileFlatIds;
|
|
44
52
|
private mapIdsToClientFields;
|
|
45
53
|
private clientKeyForDbField;
|
|
@@ -62,6 +62,13 @@ export class TableDB {
|
|
|
62
62
|
const hydratedRow = this.hydrateRow ? await this.hydrateRow(row) : row;
|
|
63
63
|
return this.transforms.parseFromDb(hydratedRow);
|
|
64
64
|
}
|
|
65
|
+
byId(id) {
|
|
66
|
+
return {
|
|
67
|
+
find: () => this.findById(id),
|
|
68
|
+
update: (data, dbOnlyData) => this.update(id, data, dbOnlyData),
|
|
69
|
+
delete: () => this.delete(id),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
65
72
|
insert(data, ...args) {
|
|
66
73
|
const dbOnlyData = args[0];
|
|
67
74
|
return {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
contains?: string;
|
|
3
|
-
startsWith?: string;
|
|
4
|
-
endsWith?: string;
|
|
1
|
+
type ComparableWhereValue<T> = {
|
|
5
2
|
gt?: T;
|
|
6
3
|
gte?: T;
|
|
7
4
|
lt?: T;
|
|
8
5
|
lte?: T;
|
|
9
|
-
in?: T[];
|
|
10
|
-
not?: T | Exclude<WhereValue<T>, T>;
|
|
11
6
|
};
|
|
7
|
+
type StringWhereValue<T> = Extract<T, string> extends never ? {} : {
|
|
8
|
+
contains?: string;
|
|
9
|
+
startsWith?: string;
|
|
10
|
+
endsWith?: string;
|
|
11
|
+
};
|
|
12
|
+
export type WhereValue<T> = T | ({
|
|
13
|
+
in?: Exclude<T, undefined>[];
|
|
14
|
+
not?: T;
|
|
15
|
+
} & ComparableWhereValue<T> & StringWhereValue<T>);
|
|
12
16
|
export type WhereInput<T> = {
|
|
13
17
|
[K in keyof T]?: WhereValue<T[K]>;
|
|
14
18
|
};
|
|
@@ -37,3 +41,4 @@ export interface TableMeta {
|
|
|
37
41
|
sqlOnlyValidators: Map<string, (val: unknown) => unknown>;
|
|
38
42
|
deriveDependencies: Map<string, string[]>;
|
|
39
43
|
}
|
|
44
|
+
export {};
|