cogsbox-shape 0.5.191 → 0.5.193
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 +105 -52
- package/cogsbox-shape-db/dist/connect.d.ts +40 -13
- 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 +13 -1
- package/cogsbox-shape-db/dist/types.d.ts +11 -6
- package/dist/generateSQL.js +148 -67
- package/dist/schema.d.ts +31 -11
- package/dist/schema.js +60 -49
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -33,12 +33,12 @@ 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. |
|
|
@@ -46,22 +46,47 @@ s.sql() → .clientInput() → .client() → .server() → .transform()
|
|
|
46
46
|
|
|
47
47
|
Note: `.derive()` is a schema-level method, 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,36 +193,45 @@ 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
|
```
|
|
180
205
|
|
|
181
|
-
#### Derived Fields (`.derive()`)
|
|
182
|
-
|
|
183
|
-
`.derive()` populates _existing fields_ dynamically
|
|
206
|
+
#### Derived Fields (`.derive()`)
|
|
207
|
+
|
|
208
|
+
`.derive()` populates _existing fields_ dynamically. Define the target field first, then choose where the derivation runs:
|
|
209
|
+
|
|
210
|
+
- `forClient` computes client-only fields during `generateDefaults()` and `toClient()`.
|
|
211
|
+
- `forDb` computes DB-backed fields during `toDb()`, `parseForDb()`, and ORM writes. Use `sqlOnly: true` when the computed column should stay hidden from the client.
|
|
184
212
|
|
|
185
213
|
```typescript
|
|
186
214
|
const users = schema({
|
|
187
215
|
_tableName: "users",
|
|
188
|
-
firstName: s.
|
|
189
|
-
lastName: s.
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
fullName: s.clientInput(""),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
216
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
217
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
|
|
218
|
+
|
|
219
|
+
// Virtual field. It exists in app/view state, not SQL.
|
|
220
|
+
fullName: s.clientInput(""),
|
|
221
|
+
|
|
222
|
+
// Hidden DB column. It is written to SQL, but not sent to the client.
|
|
223
|
+
searchIndex: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
224
|
+
}).derive({
|
|
225
|
+
forClient: {
|
|
226
|
+
fullName: (row) => `${row.firstName} ${row.lastName}`,
|
|
227
|
+
},
|
|
228
|
+
forDb: {
|
|
229
|
+
searchIndex: (row) => `${row.firstName} ${row.lastName}`.toLowerCase(),
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
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.
|
|
201
235
|
|
|
202
236
|
### Schema Object Structure
|
|
203
237
|
|
|
@@ -226,14 +260,14 @@ import { s, schema, createSchema } from "cogsbox-shape";
|
|
|
226
260
|
|
|
227
261
|
const contactSchema = schema({
|
|
228
262
|
_tableName: "contacts",
|
|
229
|
-
id: s.
|
|
263
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
230
264
|
value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
|
|
231
265
|
schema: z.string(),
|
|
232
266
|
}),
|
|
233
|
-
name: s.
|
|
234
|
-
email: s.
|
|
267
|
+
name: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.min(2)),
|
|
268
|
+
email: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.email()),
|
|
235
269
|
isActive: s
|
|
236
|
-
.
|
|
270
|
+
.sqlite({ type: "boolean", default: true })
|
|
237
271
|
.clientInput(() => z.boolean())
|
|
238
272
|
.transform({
|
|
239
273
|
toClient: (val) => Boolean(val),
|
|
@@ -273,15 +307,15 @@ import { s, schema, createSchemaBox } from "cogsbox-shape";
|
|
|
273
307
|
|
|
274
308
|
const users = schema({
|
|
275
309
|
_tableName: "users",
|
|
276
|
-
id: s.
|
|
277
|
-
name: s.
|
|
310
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
311
|
+
name: s.sqlite({ type: "varchar" }),
|
|
278
312
|
posts: s.hasMany(), // Placeholder — resolved later
|
|
279
313
|
});
|
|
280
314
|
|
|
281
315
|
const posts = schema({
|
|
282
316
|
_tableName: "posts",
|
|
283
|
-
id: s.
|
|
284
|
-
title: s.
|
|
317
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
318
|
+
title: s.sqlite({ type: "varchar" }),
|
|
285
319
|
authorId: s.reference(() => users.id), // Foreign key
|
|
286
320
|
});
|
|
287
321
|
```
|
|
@@ -337,12 +371,31 @@ type UserWithPosts = z.infer<typeof userWithPosts.schemas.client>;
|
|
|
337
371
|
// posts: { id: number; title: string; authorId: number; }[]
|
|
338
372
|
// }
|
|
339
373
|
|
|
340
|
-
// Views also have transforms for the selected fields
|
|
341
|
-
const { defaults, transforms } = userWithPosts;
|
|
342
|
-
// transforms.toClient() handles nested relation transforms automatically
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
// Views also have transforms for the selected fields
|
|
375
|
+
const { defaults, transforms } = userWithPosts;
|
|
376
|
+
// transforms.toClient() handles nested relation transforms automatically
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
When a box is connected to the ORM, view reads hydrate the selected relation tree before parsing:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { connect } from "cogsbox-shape/db";
|
|
383
|
+
import { createSqliteDb } from "cogsbox-shape/db/sqlite";
|
|
384
|
+
|
|
385
|
+
const db = createSqliteDb("app.sqlite");
|
|
386
|
+
const bx = connect(box, db);
|
|
387
|
+
|
|
388
|
+
const userView = bx.users.createView({
|
|
389
|
+
posts: true,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const user = await userView.findById(1);
|
|
393
|
+
// user.posts is loaded and validated as part of the view shape
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
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.
|
|
397
|
+
|
|
398
|
+
### 5. Nested Defaults and Form Definitions (`defaultsDefinition`)
|
|
346
399
|
|
|
347
400
|
When working with forms and nested array relations (like `hasMany`), you often need the default state for a _single new item_ to add to a form array.
|
|
348
401
|
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
} & {};
|
|
@@ -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,53 @@ 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 : TTable extends {
|
|
58
|
+
deriveDependencies: infer TDerives;
|
|
59
|
+
} ? TKey extends keyof TDerives ? true : false : false;
|
|
50
60
|
type SqlOnlyInput<T> = T extends {
|
|
51
61
|
definition: infer TDefinition;
|
|
52
62
|
} ? Prettify<{
|
|
53
|
-
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never :
|
|
63
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
|
|
64
|
+
__type: "reference";
|
|
65
|
+
} ? never : IsDerivedDbField<T, K> extends true ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? never : K : never]: SqlOnlyValue<TDefinition[K]>;
|
|
54
66
|
} & {
|
|
55
|
-
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never :
|
|
67
|
+
[K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
|
|
68
|
+
__type: "reference";
|
|
69
|
+
} ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? K : never : never]?: SqlOnlyValue<TDefinition[K]>;
|
|
56
70
|
}> : Record<string, never>;
|
|
71
|
+
type DbApiFor<T> = T extends {
|
|
72
|
+
transforms: {
|
|
73
|
+
parseForDb: (...args: any[]) => any;
|
|
74
|
+
parseFromDb: (...args: any[]) => any;
|
|
75
|
+
};
|
|
76
|
+
} ? TableDBApi<Row<Return<T["transforms"]["parseFromDb"]>>, Row<FirstArg<T["transforms"]["parseForDb"]>>, SqlOnlyInput<T>> : never;
|
|
77
|
+
type ConnectedView<T> = T extends {
|
|
78
|
+
transforms: {
|
|
79
|
+
parseForDb: (...args: any[]) => any;
|
|
80
|
+
parseFromDb: (...args: any[]) => any;
|
|
81
|
+
};
|
|
82
|
+
} ? Omit<T, keyof DbApiFor<T>> & DbApiFor<T> : T;
|
|
83
|
+
type ConnectedCreateView<T> = T extends {
|
|
84
|
+
createView: (...args: infer TArgs) => infer TView;
|
|
85
|
+
} ? {
|
|
86
|
+
createView: (...args: TArgs) => ConnectedView<TView>;
|
|
87
|
+
} : {};
|
|
57
88
|
type ConnectedTable<T> = T extends {
|
|
58
89
|
transforms: {
|
|
59
90
|
parseForDb: (...args: any[]) => any;
|
|
60
91
|
parseFromDb: (...args: any[]) => any;
|
|
61
92
|
};
|
|
62
|
-
} ? T &
|
|
63
|
-
db: TableDB<Return<T["transforms"]["parseFromDb"]>, FirstArg<T["transforms"]["parseForDb"]>, SqlOnlyInput<T>>;
|
|
64
|
-
} : T;
|
|
93
|
+
} ? Omit<T, "createView" | keyof DbApiFor<T>> & DbApiFor<T> & ConnectedCreateView<T> : T;
|
|
65
94
|
type ConnectedBox<T extends Record<string, unknown>> = {
|
|
66
95
|
[K in keyof T]: ConnectedTable<T[K]>;
|
|
67
96
|
} & {
|
|
68
|
-
|
|
69
|
-
transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
|
|
70
|
-
};
|
|
97
|
+
transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
|
|
71
98
|
};
|
|
72
|
-
export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<
|
|
99
|
+
export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<any>): ConnectedBox<T>;
|
|
73
100
|
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 {
|
|
@@ -80,6 +87,7 @@ export class TableDB {
|
|
|
80
87
|
const dbData = this.transforms.parseForDb(data);
|
|
81
88
|
const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
|
|
82
89
|
requireRequired: true,
|
|
90
|
+
presentDbData: dbData,
|
|
83
91
|
});
|
|
84
92
|
const clientPkClientKeys = this.meta.clientPkFields;
|
|
85
93
|
const pkDbNames = new Set(clientPkClientKeys.map((k) => {
|
|
@@ -225,7 +233,11 @@ export class TableDB {
|
|
|
225
233
|
parseDbOnlyData(dbOnlyData, opts = { requireRequired: false }) {
|
|
226
234
|
if (opts.requireRequired) {
|
|
227
235
|
for (const requiredKey of this.meta.sqlOnlyRequiredClientFields) {
|
|
228
|
-
|
|
236
|
+
const field = this.meta.dbFields.get(requiredKey);
|
|
237
|
+
const dbName = field?.dbName ?? requiredKey;
|
|
238
|
+
const alreadyPresent = opts.presentDbData?.[dbName] !== undefined;
|
|
239
|
+
if (!alreadyPresent &&
|
|
240
|
+
(!dbOnlyData || dbOnlyData[requiredKey] === undefined)) {
|
|
229
241
|
throw new Error(`Missing required sqlOnly field "${requiredKey}" for "${this.meta.tableName}".`);
|
|
230
242
|
}
|
|
231
243
|
}
|
|
@@ -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 {};
|
package/dist/generateSQL.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
|
-
const sqlTypeMap = {
|
|
3
|
-
int: "INTEGER",
|
|
4
|
-
varchar: (length = 255) => `VARCHAR(${length})`,
|
|
5
|
-
char: (length = 1) => `CHAR(${length})`,
|
|
6
|
-
text: "TEXT",
|
|
7
|
-
longtext: "LONGTEXT",
|
|
8
|
-
boolean: "TINYINT(1)",
|
|
9
|
-
date: "DATE",
|
|
10
|
-
datetime: "DATETIME",
|
|
11
|
-
};
|
|
12
2
|
function isWrappedSchema(input) {
|
|
13
3
|
return (input !== null &&
|
|
14
4
|
typeof input === "object" &&
|
|
@@ -16,6 +6,107 @@ function isWrappedSchema(input) {
|
|
|
16
6
|
input.schemas !== null &&
|
|
17
7
|
typeof input.schemas === "object");
|
|
18
8
|
}
|
|
9
|
+
function escapeSqlString(value) {
|
|
10
|
+
return value.replace(/'/g, "''");
|
|
11
|
+
}
|
|
12
|
+
function quoteEnumValues(values) {
|
|
13
|
+
return values.map((value) => `'${escapeSqlString(value)}'`).join(", ");
|
|
14
|
+
}
|
|
15
|
+
function columnName(fieldName, sqlConfig) {
|
|
16
|
+
return sqlConfig.field ?? fieldName;
|
|
17
|
+
}
|
|
18
|
+
function assertDialect(current, next, tableName) {
|
|
19
|
+
if (current && current !== next) {
|
|
20
|
+
throw new Error(`Mixed SQL dialects in table "${tableName}": "${current}" and "${next}".`);
|
|
21
|
+
}
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
function sqlType(dialect, fieldName, tableName, config) {
|
|
25
|
+
switch (dialect) {
|
|
26
|
+
case "sqlite":
|
|
27
|
+
switch (config.type) {
|
|
28
|
+
case "int":
|
|
29
|
+
return "INTEGER";
|
|
30
|
+
case "boolean":
|
|
31
|
+
return "INTEGER";
|
|
32
|
+
case "varchar":
|
|
33
|
+
case "char":
|
|
34
|
+
case "text":
|
|
35
|
+
case "longtext":
|
|
36
|
+
case "enum":
|
|
37
|
+
return "TEXT";
|
|
38
|
+
case "date":
|
|
39
|
+
case "datetime":
|
|
40
|
+
case "timestamp":
|
|
41
|
+
return "TEXT";
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case "postgres":
|
|
45
|
+
switch (config.type) {
|
|
46
|
+
case "int":
|
|
47
|
+
return "INTEGER";
|
|
48
|
+
case "boolean":
|
|
49
|
+
return "BOOLEAN";
|
|
50
|
+
case "varchar":
|
|
51
|
+
return `VARCHAR(${config.length ?? 255})`;
|
|
52
|
+
case "char":
|
|
53
|
+
return `CHAR(${config.length ?? 1})`;
|
|
54
|
+
case "text":
|
|
55
|
+
case "longtext":
|
|
56
|
+
return "TEXT";
|
|
57
|
+
case "enum":
|
|
58
|
+
if (!config.name) {
|
|
59
|
+
throw new Error(`Postgres enum field "${tableName}.${fieldName}" requires a name.`);
|
|
60
|
+
}
|
|
61
|
+
return config.name;
|
|
62
|
+
case "date":
|
|
63
|
+
return "DATE";
|
|
64
|
+
case "datetime":
|
|
65
|
+
case "timestamp":
|
|
66
|
+
return "TIMESTAMP";
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case "mysql":
|
|
70
|
+
switch (config.type) {
|
|
71
|
+
case "int":
|
|
72
|
+
return "INTEGER";
|
|
73
|
+
case "boolean":
|
|
74
|
+
return "TINYINT(1)";
|
|
75
|
+
case "varchar":
|
|
76
|
+
return `VARCHAR(${config.length ?? 255})`;
|
|
77
|
+
case "char":
|
|
78
|
+
return `CHAR(${config.length ?? 1})`;
|
|
79
|
+
case "text":
|
|
80
|
+
return "TEXT";
|
|
81
|
+
case "longtext":
|
|
82
|
+
return "LONGTEXT";
|
|
83
|
+
case "enum":
|
|
84
|
+
return `ENUM(${quoteEnumValues(config.values ?? [])})`;
|
|
85
|
+
case "date":
|
|
86
|
+
return "DATE";
|
|
87
|
+
case "datetime":
|
|
88
|
+
return "DATETIME";
|
|
89
|
+
case "timestamp":
|
|
90
|
+
return "TIMESTAMP";
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Unknown ${dialect} SQL type "${config.type}" for field "${tableName}.${fieldName}".`);
|
|
95
|
+
}
|
|
96
|
+
function defaultSql(value) {
|
|
97
|
+
if (value === "CURRENT_TIMESTAMP")
|
|
98
|
+
return "CURRENT_TIMESTAMP";
|
|
99
|
+
if (typeof value === "string")
|
|
100
|
+
return `'${escapeSqlString(value)}'`;
|
|
101
|
+
if (value instanceof Date)
|
|
102
|
+
return `'${value.toISOString()}'`;
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
function enumCheck(dialect, fieldName, config) {
|
|
106
|
+
if (dialect !== "sqlite" || config.type !== "enum")
|
|
107
|
+
return undefined;
|
|
108
|
+
return `CHECK (${fieldName} IN (${quoteEnumValues(config.values ?? [])}))`;
|
|
109
|
+
}
|
|
19
110
|
export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", options = { includeForeignKeys: true }) {
|
|
20
111
|
if (!input) {
|
|
21
112
|
throw new Error("No schema input provided");
|
|
@@ -24,7 +115,8 @@ export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", o
|
|
|
24
115
|
if (!schemas || typeof schemas !== "object") {
|
|
25
116
|
throw new Error("Invalid schemas input");
|
|
26
117
|
}
|
|
27
|
-
const
|
|
118
|
+
const statements = [];
|
|
119
|
+
const postgresEnums = new Map();
|
|
28
120
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
29
121
|
const tableName = schema._tableName;
|
|
30
122
|
if (!tableName) {
|
|
@@ -33,91 +125,80 @@ export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", o
|
|
|
33
125
|
}
|
|
34
126
|
const fields = [];
|
|
35
127
|
const foreignKeys = [];
|
|
128
|
+
let tableDialect;
|
|
36
129
|
for (const [fieldName, field] of Object.entries(schema)) {
|
|
37
|
-
|
|
38
|
-
const f = field; // Just cast once
|
|
39
|
-
console.log(`Processing field: ${fieldName}`, f);
|
|
40
|
-
// Skip metadata fields
|
|
130
|
+
const f = field;
|
|
41
131
|
if (fieldName === "_tableName" ||
|
|
42
132
|
fieldName === "SchemaWrapperBrand" ||
|
|
43
133
|
fieldName.startsWith("__") ||
|
|
44
134
|
typeof f !== "object" ||
|
|
45
|
-
!f)
|
|
135
|
+
!f) {
|
|
46
136
|
continue;
|
|
47
|
-
|
|
137
|
+
}
|
|
48
138
|
if (f.type === "reference" && f.to) {
|
|
49
139
|
const referencedField = f.to();
|
|
50
140
|
const targetTableName = referencedField.__parentTableType._tableName;
|
|
51
141
|
const targetFieldName = referencedField.__meta._key;
|
|
52
|
-
console.log(`Found reference field: ${fieldName} -> ${targetTableName}.${targetFieldName}`);
|
|
53
142
|
fields.push(` ${fieldName} INTEGER NOT NULL`);
|
|
54
143
|
if (options.includeForeignKeys) {
|
|
55
144
|
foreignKeys.push(` FOREIGN KEY (${fieldName}) REFERENCES ${targetTableName}(${targetFieldName})`);
|
|
56
145
|
}
|
|
57
146
|
continue;
|
|
58
147
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (sqlConfig.type === "belongsTo" &&
|
|
72
|
-
sqlConfig.fromKey &&
|
|
73
|
-
sqlConfig.schema) {
|
|
74
|
-
fields.push(` ${sqlConfig.fromKey} INTEGER`);
|
|
75
|
-
if (options.includeForeignKeys) {
|
|
76
|
-
const targetSchema = sqlConfig.schema();
|
|
77
|
-
foreignKeys.push(` FOREIGN KEY (${sqlConfig.fromKey}) REFERENCES ${targetSchema._tableName}(id)`);
|
|
78
|
-
}
|
|
148
|
+
const fieldDef = f.__meta?._fieldType ?? f;
|
|
149
|
+
const sqlConfig = fieldDef?.config?.sql;
|
|
150
|
+
if (!sqlConfig)
|
|
151
|
+
continue;
|
|
152
|
+
if (["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
|
|
153
|
+
if (sqlConfig.type === "belongsTo" &&
|
|
154
|
+
sqlConfig.fromKey &&
|
|
155
|
+
sqlConfig.schema) {
|
|
156
|
+
fields.push(` ${sqlConfig.fromKey} INTEGER`);
|
|
157
|
+
if (options.includeForeignKeys) {
|
|
158
|
+
const targetSchema = sqlConfig.schema();
|
|
159
|
+
foreignKeys.push(` FOREIGN KEY (${sqlConfig.fromKey}) REFERENCES ${targetSchema._tableName}(id)`);
|
|
79
160
|
}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
// Handle regular SQL types
|
|
83
|
-
const { type, nullable, pk, length, default: defaultValue } = sqlConfig;
|
|
84
|
-
if (!sqlTypeMap[type]) {
|
|
85
|
-
console.warn(`Unknown SQL type: ${type} for field ${fieldName}`);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const sqlType = typeof sqlTypeMap[type] === "function"
|
|
89
|
-
? sqlTypeMap[type](length)
|
|
90
|
-
: sqlTypeMap[type];
|
|
91
|
-
let fieldDefStr = ` ${fieldName} ${sqlType}`;
|
|
92
|
-
if (pk)
|
|
93
|
-
fieldDefStr += " PRIMARY KEY AUTO_INCREMENT";
|
|
94
|
-
if (!nullable && !pk)
|
|
95
|
-
fieldDefStr += " NOT NULL";
|
|
96
|
-
// Handle defaults
|
|
97
|
-
if (defaultValue !== undefined &&
|
|
98
|
-
defaultValue !== "CURRENT_TIMESTAMP") {
|
|
99
|
-
fieldDefStr += ` DEFAULT ${typeof defaultValue === "string" ? `'${defaultValue}'` : defaultValue}`;
|
|
100
161
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const dialect = sqlConfig.dialect;
|
|
165
|
+
if (!dialect) {
|
|
166
|
+
throw new Error(`Field "${tableName}.${fieldName}" is missing a SQL dialect.`);
|
|
167
|
+
}
|
|
168
|
+
tableDialect = assertDialect(tableDialect, dialect, tableName);
|
|
169
|
+
if (dialect === "postgres" && sqlConfig.type === "enum") {
|
|
170
|
+
postgresEnums.set(sqlConfig.name, sqlConfig.values);
|
|
171
|
+
}
|
|
172
|
+
const dbFieldName = columnName(fieldName, sqlConfig);
|
|
173
|
+
const parts = [
|
|
174
|
+
dbFieldName,
|
|
175
|
+
sqlType(dialect, fieldName, tableName, sqlConfig),
|
|
176
|
+
];
|
|
177
|
+
if (sqlConfig.pk) {
|
|
178
|
+
parts.push(dialect === "mysql" ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY");
|
|
179
|
+
}
|
|
180
|
+
if (!sqlConfig.nullable && !sqlConfig.pk)
|
|
181
|
+
parts.push("NOT NULL");
|
|
182
|
+
if (sqlConfig.default !== undefined) {
|
|
183
|
+
parts.push(`DEFAULT ${defaultSql(sqlConfig.default)}`);
|
|
105
184
|
}
|
|
185
|
+
const check = enumCheck(dialect, dbFieldName, sqlConfig);
|
|
186
|
+
if (check)
|
|
187
|
+
parts.push(check);
|
|
188
|
+
fields.push(` ${parts.join(" ")}`);
|
|
106
189
|
}
|
|
107
|
-
// Combine fields and foreign keys based on option
|
|
108
190
|
const allFields = options.includeForeignKeys
|
|
109
191
|
? [...fields, ...foreignKeys]
|
|
110
192
|
: fields;
|
|
111
|
-
// Create table SQL
|
|
112
193
|
if (allFields.length > 0) {
|
|
113
|
-
|
|
194
|
+
statements.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);`);
|
|
114
195
|
}
|
|
115
196
|
else {
|
|
116
197
|
console.warn(`Warning: Table ${tableName} has no fields`);
|
|
117
198
|
}
|
|
118
199
|
}
|
|
119
|
-
|
|
120
|
-
const sqlContent =
|
|
200
|
+
const enumStatements = Array.from(postgresEnums.entries()).map(([name, values]) => `CREATE TYPE ${name} AS ENUM (${quoteEnumValues(values)});`);
|
|
201
|
+
const sqlContent = [...enumStatements, ...statements].join("\n\n");
|
|
121
202
|
await fs.writeFile(outputPath, sqlContent, "utf-8");
|
|
122
203
|
return sqlContent;
|
|
123
204
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ type CurrentTimestampConfig = {
|
|
|
9
9
|
export declare const isFunction: (fn: unknown) => fn is Function;
|
|
10
10
|
export declare function currentTimeStamp(): CurrentTimestampConfig;
|
|
11
11
|
type DbConfig = SQLType | RelationConfig<any> | null;
|
|
12
|
-
export type
|
|
12
|
+
export type SQLDialect = "sqlite" | "postgres" | "mysql";
|
|
13
|
+
type SQLTypeConfig = ({
|
|
13
14
|
type: "int";
|
|
14
15
|
nullable?: boolean;
|
|
15
16
|
default?: number;
|
|
@@ -31,16 +32,33 @@ export type SQLType = ({
|
|
|
31
32
|
nullable?: boolean;
|
|
32
33
|
length?: number;
|
|
33
34
|
default?: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "enum";
|
|
37
|
+
values: readonly [string, ...string[]];
|
|
38
|
+
nullable?: boolean;
|
|
39
|
+
default?: string;
|
|
40
|
+
name?: string;
|
|
34
41
|
}) & BaseConfig;
|
|
42
|
+
export type SQLType = SQLTypeConfig & {
|
|
43
|
+
dialect: SQLDialect;
|
|
44
|
+
};
|
|
45
|
+
type SQLTypeInput = SQLTypeConfig;
|
|
46
|
+
type WithDialect<T extends SQLTypeInput, TDialect extends SQLDialect> = SQLType & T & {
|
|
47
|
+
dialect: TDialect;
|
|
48
|
+
};
|
|
35
49
|
type BaseConfig = {
|
|
36
50
|
nullable?: boolean;
|
|
37
51
|
pk?: true;
|
|
38
52
|
field?: string;
|
|
39
53
|
sqlOnly?: true;
|
|
40
54
|
};
|
|
41
|
-
type SQLToZodType<T extends
|
|
55
|
+
type SQLToZodType<T extends SQLTypeInput, TDefault extends boolean> = T["pk"] extends true ? TDefault extends true ? z.ZodString : z.ZodNumber : T["nullable"] extends true ? T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodNullable<z.ZodString> : T["type"] extends "enum" ? T extends {
|
|
56
|
+
values: infer TValues extends readonly [string, ...string[]];
|
|
57
|
+
} ? z.ZodNullable<z.ZodType<TValues[number]>> : never : T["type"] extends "int" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "boolean" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
|
|
42
58
|
default: "CURRENT_TIMESTAMP";
|
|
43
|
-
} ? TDefault extends true ? never : z.ZodNullable<z.ZodDate> : z.ZodNullable<z.ZodDate> : never : T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodString : T["type"] extends "
|
|
59
|
+
} ? TDefault extends true ? never : z.ZodNullable<z.ZodDate> : z.ZodNullable<z.ZodDate> : never : T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodString : T["type"] extends "enum" ? T extends {
|
|
60
|
+
values: infer TValues extends readonly [string, ...string[]];
|
|
61
|
+
} ? z.ZodType<TValues[number]> : never : T["type"] extends "int" ? z.ZodNumber : T["type"] extends "boolean" ? z.ZodNumber : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
|
|
44
62
|
default: "CURRENT_TIMESTAMP";
|
|
45
63
|
} ? TDefault extends true ? never : z.ZodDate : z.ZodDate : never;
|
|
46
64
|
type ZodTypeFromPrimitive<T> = T extends string ? z.ZodString : T extends number ? z.ZodNumber : T extends boolean ? z.ZodBoolean : T extends Date ? z.ZodDate : z.ZodAny;
|
|
@@ -164,7 +182,9 @@ interface ShapeAPI {
|
|
|
164
182
|
clientInput: <const TValue>(value: TValue | ((tools: {
|
|
165
183
|
uuid: () => string;
|
|
166
184
|
}) => TValue)) => Builder<"clientInput", null, z.ZodUndefined, TValue extends () => infer R ? R : TValue, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>;
|
|
167
|
-
|
|
185
|
+
sqlite: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "sqlite">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
|
|
186
|
+
postgres: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "postgres">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
|
|
187
|
+
mysql: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "mysql">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
|
|
168
188
|
reference: <TGetter extends () => any>(getter: TGetter) => Reference<TGetter>;
|
|
169
189
|
hasMany: <T extends HasManyDefault>(config?: T) => PlaceholderRelation<"hasMany">;
|
|
170
190
|
hasOne: (config?: HasOneDefault) => PlaceholderRelation<"hasOne">;
|
|
@@ -201,14 +221,14 @@ type PickClientOnlyKeys<T extends ShapeSchema> = {
|
|
|
201
221
|
};
|
|
202
222
|
} ? K : never;
|
|
203
223
|
}[keyof T];
|
|
204
|
-
type
|
|
224
|
+
type PickDbFieldKeys<T extends ShapeSchema> = {
|
|
205
225
|
[K in keyof T]: T[K] extends {
|
|
206
226
|
config: {
|
|
207
|
-
sql:
|
|
208
|
-
sqlOnly: true;
|
|
209
|
-
};
|
|
227
|
+
sql: infer TSql;
|
|
210
228
|
};
|
|
211
|
-
} ?
|
|
229
|
+
} ? TSql extends null ? never : TSql extends {
|
|
230
|
+
type: "hasMany" | "hasOne" | "belongsTo" | "manyToMany";
|
|
231
|
+
} ? never : K : never;
|
|
212
232
|
}[keyof T];
|
|
213
233
|
type InferClientRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodClientSchema">>>>>;
|
|
214
234
|
type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
|
|
@@ -223,7 +243,7 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
|
|
|
223
243
|
[K in PickClientOnlyKeys<T>]?: (row: InferClientRow<T>) => any;
|
|
224
244
|
};
|
|
225
245
|
forDb?: {
|
|
226
|
-
[K in
|
|
246
|
+
[K in PickDbFieldKeys<T>]?: (row: InferClientRow<T>) => any;
|
|
227
247
|
};
|
|
228
248
|
}) => SchemaBuilder<T>;
|
|
229
249
|
};
|
|
@@ -245,7 +265,7 @@ export type Schema<T extends Record<string, SchemaField | (() => Relation<any>)>
|
|
|
245
265
|
__schemaId?: string;
|
|
246
266
|
[key: string]: T[keyof T] | string | ((id: number) => string) | true | undefined;
|
|
247
267
|
};
|
|
248
|
-
type ValidShapeField = ReturnType<typeof s.
|
|
268
|
+
type ValidShapeField = ReturnType<typeof s.sqlite> | ReturnType<typeof s.postgres> | ReturnType<typeof s.mysql>;
|
|
249
269
|
export type ShapeSchema<T extends string = string> = {
|
|
250
270
|
_tableName: T;
|
|
251
271
|
[SchemaWrapperBrand]?: true;
|
package/dist/schema.js
CHANGED
|
@@ -7,6 +7,48 @@ export function currentTimeStamp() {
|
|
|
7
7
|
defaultValue: new Date(),
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
+
function createSqlBuilder(dialect, sqlConfig) {
|
|
11
|
+
const sqlZodType = (() => {
|
|
12
|
+
let baseType;
|
|
13
|
+
if (sqlConfig.pk) {
|
|
14
|
+
baseType = z.number();
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
switch (sqlConfig.type) {
|
|
18
|
+
case "int":
|
|
19
|
+
baseType = z.number();
|
|
20
|
+
break;
|
|
21
|
+
case "boolean":
|
|
22
|
+
baseType = z.number();
|
|
23
|
+
break;
|
|
24
|
+
case "date":
|
|
25
|
+
case "datetime":
|
|
26
|
+
case "timestamp":
|
|
27
|
+
baseType = z.date();
|
|
28
|
+
break;
|
|
29
|
+
case "enum":
|
|
30
|
+
baseType = z.enum(sqlConfig.values);
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
baseType = z.string();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (sqlConfig.nullable) {
|
|
38
|
+
baseType = baseType.nullable();
|
|
39
|
+
}
|
|
40
|
+
return baseType;
|
|
41
|
+
})();
|
|
42
|
+
const dialectConfig = { ...sqlConfig, dialect };
|
|
43
|
+
return createBuilder({
|
|
44
|
+
stage: "sql",
|
|
45
|
+
sqlConfig: dialectConfig,
|
|
46
|
+
sqlZod: sqlZodType,
|
|
47
|
+
initialValue: inferDefaultFromZod(sqlZodType, dialectConfig),
|
|
48
|
+
clientZod: sqlZodType,
|
|
49
|
+
validationZod: sqlZodType,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
10
52
|
export const s = {
|
|
11
53
|
clientInput: (value) => {
|
|
12
54
|
const sample = isFunction(value) ? value({ uuid }) : value;
|
|
@@ -60,44 +102,9 @@ export const s = {
|
|
|
60
102
|
relationType: "manyToMany",
|
|
61
103
|
defaultCount: config?.defaultCount ?? 0,
|
|
62
104
|
}),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (sqlConfig.pk) {
|
|
67
|
-
baseType = z.number();
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
switch (sqlConfig.type) {
|
|
71
|
-
case "int":
|
|
72
|
-
baseType = z.number();
|
|
73
|
-
break;
|
|
74
|
-
case "boolean":
|
|
75
|
-
baseType = z.number();
|
|
76
|
-
break;
|
|
77
|
-
case "date":
|
|
78
|
-
case "datetime":
|
|
79
|
-
case "timestamp":
|
|
80
|
-
baseType = z.date();
|
|
81
|
-
break;
|
|
82
|
-
default:
|
|
83
|
-
baseType = z.string();
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (sqlConfig.nullable) {
|
|
88
|
-
baseType = baseType.nullable();
|
|
89
|
-
}
|
|
90
|
-
return baseType;
|
|
91
|
-
})();
|
|
92
|
-
return createBuilder({
|
|
93
|
-
stage: "sql",
|
|
94
|
-
sqlConfig: sqlConfig,
|
|
95
|
-
sqlZod: sqlZodType,
|
|
96
|
-
initialValue: inferDefaultFromZod(sqlZodType, sqlConfig),
|
|
97
|
-
clientZod: sqlZodType,
|
|
98
|
-
validationZod: sqlZodType,
|
|
99
|
-
});
|
|
100
|
-
},
|
|
105
|
+
sqlite: (sqlConfig) => createSqlBuilder("sqlite", sqlConfig),
|
|
106
|
+
postgres: (sqlConfig) => createSqlBuilder("postgres", sqlConfig),
|
|
107
|
+
mysql: (sqlConfig) => createSqlBuilder("mysql", sqlConfig),
|
|
101
108
|
};
|
|
102
109
|
function createBuilder(config) {
|
|
103
110
|
const completedStages = config.completedStages || new Set([config.stage]);
|
|
@@ -387,6 +394,8 @@ function inferDefaultFromZod(zodType, sqlConfig) {
|
|
|
387
394
|
case "char":
|
|
388
395
|
case "longtext":
|
|
389
396
|
return "";
|
|
397
|
+
case "enum":
|
|
398
|
+
return sqlTypeConfig.default ?? sqlTypeConfig.values[0];
|
|
390
399
|
case "int":
|
|
391
400
|
return 0;
|
|
392
401
|
case "boolean":
|
|
@@ -622,7 +631,9 @@ export function createSchema(schema, relations) {
|
|
|
622
631
|
for (const clientKey in clientObject) {
|
|
623
632
|
if (clientObject[clientKey] === undefined)
|
|
624
633
|
continue;
|
|
625
|
-
const dbKey = clientToDbKeys[clientKey]
|
|
634
|
+
const dbKey = clientToDbKeys[clientKey];
|
|
635
|
+
if (!dbKey)
|
|
636
|
+
continue;
|
|
626
637
|
const transform = fieldTransforms[clientKey]?.toDb;
|
|
627
638
|
dbObject[dbKey] = transform
|
|
628
639
|
? transform(clientObject[clientKey])
|
|
@@ -631,7 +642,7 @@ export function createSchema(schema, relations) {
|
|
|
631
642
|
// 2. Map Database ONLY derives directly to the dbObject
|
|
632
643
|
if (derives?.forDb) {
|
|
633
644
|
for (const schemaKey in derives.forDb) {
|
|
634
|
-
// Resolve custom DB column name if they used s.
|
|
645
|
+
// Resolve custom DB column name if they used s.sqlite({ field: "custom_name" })
|
|
635
646
|
const sqlConfig = fullSchema[schemaKey]?.config?.sql;
|
|
636
647
|
const dbKey = sqlConfig?.field || schemaKey;
|
|
637
648
|
dbObject[dbKey] = derives.forDb[schemaKey]?.(clientObject);
|
|
@@ -644,9 +655,11 @@ export function createSchema(schema, relations) {
|
|
|
644
655
|
const finalClientSchema = z.object(clientFields);
|
|
645
656
|
const finalValidationSchema = z.object(serverFields);
|
|
646
657
|
const deriveDependencies = {};
|
|
647
|
-
|
|
658
|
+
const trackDeriveDependencies = (deriveGroup) => {
|
|
659
|
+
if (!deriveGroup)
|
|
660
|
+
return;
|
|
648
661
|
const trackingSeed = { ...defaultValues };
|
|
649
|
-
for (const key in
|
|
662
|
+
for (const key in deriveGroup) {
|
|
650
663
|
const accessed = new Set();
|
|
651
664
|
const trackingRow = new Proxy(trackingSeed, {
|
|
652
665
|
get(target, prop, receiver) {
|
|
@@ -657,12 +670,14 @@ export function createSchema(schema, relations) {
|
|
|
657
670
|
},
|
|
658
671
|
});
|
|
659
672
|
try {
|
|
660
|
-
|
|
673
|
+
deriveGroup[key]?.(trackingRow);
|
|
661
674
|
}
|
|
662
675
|
catch (e) { }
|
|
663
|
-
deriveDependencies[key] = Array.from(accessed);
|
|
676
|
+
deriveDependencies[key] = Array.from(new Set([...(deriveDependencies[key] ?? []), ...accessed]));
|
|
664
677
|
}
|
|
665
|
-
}
|
|
678
|
+
};
|
|
679
|
+
trackDeriveDependencies(derives?.forClient);
|
|
680
|
+
trackDeriveDependencies(derives?.forDb);
|
|
666
681
|
return {
|
|
667
682
|
pk: pkKeys.length ? pkKeys : null,
|
|
668
683
|
clientPk: clientPkKeys.length ? clientPkKeys : null,
|
|
@@ -706,10 +721,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
|
|
|
706
721
|
registryEntry.zodSchemas.clientPk.length > 0);
|
|
707
722
|
checkedTables[currentRegistryKey] = hasPks;
|
|
708
723
|
if (!hasPks) {
|
|
709
|
-
console.log(`Table ${currentRegistryKey} missing pk/clientPk:`, {
|
|
710
|
-
pk: registryEntry.zodSchemas?.pk,
|
|
711
|
-
clientPk: registryEntry.zodSchemas?.clientPk,
|
|
712
|
-
});
|
|
713
724
|
allTablesSupportsReconciliation = false;
|
|
714
725
|
}
|
|
715
726
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogsbox-shape",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.193",
|
|
4
4
|
"description": "A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for relationships and serialization.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,16 +22,20 @@
|
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
|
-
"import": "./dist/index.js"
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
26
27
|
},
|
|
27
28
|
"./db": {
|
|
28
29
|
"types": "./cogsbox-shape-db/dist/index.d.ts",
|
|
29
|
-
"import": "./cogsbox-shape-db/dist/index.js"
|
|
30
|
+
"import": "./cogsbox-shape-db/dist/index.js",
|
|
31
|
+
"default": "./cogsbox-shape-db/dist/index.js"
|
|
30
32
|
},
|
|
31
33
|
"./db/sqlite": {
|
|
32
34
|
"types": "./cogsbox-shape-db/dist/sqlite/index.d.ts",
|
|
33
|
-
"import": "./cogsbox-shape-db/dist/sqlite/index.js"
|
|
34
|
-
|
|
35
|
+
"import": "./cogsbox-shape-db/dist/sqlite/index.js",
|
|
36
|
+
"default": "./cogsbox-shape-db/dist/sqlite/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
35
39
|
},
|
|
36
40
|
"keywords": [
|
|
37
41
|
"typescript",
|