cogsbox-shape 0.5.154 → 0.5.156
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 +54 -66
- package/dist/schema.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ cogsbox-shape introduces a unified flow that mirrors how data moves through your
|
|
|
35
35
|
```
|
|
36
36
|
Initial State
|
|
37
37
|
\
|
|
38
|
-
SQL ←→ Transform ←→ Client ←→ Validation
|
|
38
|
+
SQL ←→ Transform ←→ Client ←→ Server (Validation)
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
This flow ensures type safety at every step while giving you control over transformations.
|
|
@@ -59,25 +59,35 @@ const userSchema = schema({
|
|
|
59
59
|
|
|
60
60
|
### 2. Initial State - Define Creation Defaults
|
|
61
61
|
|
|
62
|
-
When creating new records, you often need different types than what's stored in the database
|
|
62
|
+
When creating new records, you often need different types than what's stored in the database.
|
|
63
|
+
|
|
64
|
+
**Note:** `initialState` takes an object configuration where you provide the runtime `value` and optionally the Zod `schema`.
|
|
63
65
|
|
|
64
66
|
```typescript
|
|
67
|
+
import { z } from "zod";
|
|
68
|
+
|
|
65
69
|
const userSchema = schema({
|
|
66
70
|
_tableName: "users",
|
|
67
|
-
id: s.sql({ type: "int", pk: true }).initialState(() => crypto.randomUUID()),
|
|
68
71
|
// DB stores integers, but new records start with UUID strings
|
|
72
|
+
id: s.sql({ type: "int", pk: true }).initialState({
|
|
73
|
+
value: () => crypto.randomUUID(),
|
|
74
|
+
schema: z.string(),
|
|
75
|
+
}),
|
|
69
76
|
// This automatically creates a union type: number | string on the client
|
|
70
77
|
});
|
|
71
78
|
```
|
|
72
79
|
|
|
73
80
|
### 3. Client - Define Client Representation
|
|
74
81
|
|
|
75
|
-
Transform how data appears to clients
|
|
82
|
+
Transform how data appears to clients using `.client()`:
|
|
76
83
|
|
|
77
84
|
```typescript
|
|
78
85
|
const productSchema = schema({
|
|
79
86
|
_tableName: "products",
|
|
80
|
-
id: s.sql({ type: "int", pk: true }).initialState(
|
|
87
|
+
id: s.sql({ type: "int", pk: true }).initialState({
|
|
88
|
+
value: () => `tmp_${Date.now()}`,
|
|
89
|
+
schema: z.string(),
|
|
90
|
+
}),
|
|
81
91
|
|
|
82
92
|
price: s
|
|
83
93
|
.sql({ type: "int" }) // Stored as cents in DB
|
|
@@ -89,18 +99,18 @@ const productSchema = schema({
|
|
|
89
99
|
});
|
|
90
100
|
```
|
|
91
101
|
|
|
92
|
-
### 4.
|
|
102
|
+
### 4. Server - Define Validation Rules
|
|
93
103
|
|
|
94
|
-
Add validation that runs at your client -> server boundary
|
|
104
|
+
Add validation that runs at your client -> server boundary using `.server()`:
|
|
95
105
|
|
|
96
106
|
```typescript
|
|
97
107
|
const userSchema = schema({
|
|
98
108
|
_tableName: "users",
|
|
99
109
|
email: s
|
|
100
110
|
.sql({ type: "varchar", length: 255 })
|
|
101
|
-
.
|
|
111
|
+
.server(({ sql }) => sql.email().toLowerCase()),
|
|
102
112
|
|
|
103
|
-
age: s.sql({ type: "int" }).
|
|
113
|
+
age: s.sql({ type: "int" }).server(({ sql }) => sql.min(18).max(120)),
|
|
104
114
|
});
|
|
105
115
|
```
|
|
106
116
|
|
|
@@ -159,25 +169,22 @@ const box = createSchemaBox({ users, posts }, (s) => ({
|
|
|
159
169
|
}));
|
|
160
170
|
```
|
|
161
171
|
|
|
162
|
-
### 3. Access Base Schemas
|
|
172
|
+
### 3. Access Base Schemas
|
|
163
173
|
|
|
164
|
-
|
|
174
|
+
By default, the schemas accessed directly on the box **exclude relations**. This prevents circular dependencies and over-fetching.
|
|
165
175
|
|
|
166
176
|
```typescript
|
|
167
177
|
// Access the processed schemas for the 'users' table
|
|
168
178
|
const userSchemas = box.users.schemas;
|
|
169
|
-
const userDefaults = box.users.defaults;
|
|
170
|
-
|
|
171
|
-
// Type-safe operations
|
|
172
|
-
const newUser = userDefaults; // { id: 0, name: '' }
|
|
173
179
|
|
|
174
180
|
// The base schema does NOT include the 'posts' relation
|
|
175
|
-
type UserClient = z.infer<typeof userSchemas.client>;
|
|
181
|
+
type UserClient = z.infer<typeof userSchemas.client>;
|
|
182
|
+
// { id: number; name: string; }
|
|
176
183
|
```
|
|
177
184
|
|
|
178
185
|
### 4. Create Views to Include Relations
|
|
179
186
|
|
|
180
|
-
|
|
187
|
+
To include relationships, you must explicitly create a view. This ensures you only load the data you need.
|
|
181
188
|
|
|
182
189
|
```typescript
|
|
183
190
|
// Create a view that includes the 'posts' for a user
|
|
@@ -186,7 +193,7 @@ const userWithPostsView = box.users.createView({
|
|
|
186
193
|
});
|
|
187
194
|
|
|
188
195
|
// The type of this view now includes the nested posts
|
|
189
|
-
type UserWithPosts = z.infer<typeof userWithPostsView.client>;
|
|
196
|
+
type UserWithPosts = z.infer<typeof userWithPostsView.schemas.client>;
|
|
190
197
|
// {
|
|
191
198
|
// id: number;
|
|
192
199
|
// name: string;
|
|
@@ -197,8 +204,8 @@ type UserWithPosts = z.infer<typeof userWithPostsView.client>;
|
|
|
197
204
|
// }[];
|
|
198
205
|
// }
|
|
199
206
|
|
|
200
|
-
// You can also get default values for
|
|
201
|
-
const
|
|
207
|
+
// You can also get default values specifically for this view
|
|
208
|
+
const defaults = userWithPostsView.defaults;
|
|
202
209
|
// { id: 0, name: '', posts: [] }
|
|
203
210
|
```
|
|
204
211
|
|
|
@@ -207,17 +214,19 @@ const newUserWithPosts = userWithPostsView.defaults;
|
|
|
207
214
|
Here's a complete example showing the power of the flow:
|
|
208
215
|
|
|
209
216
|
```typescript
|
|
210
|
-
import { s, schema, createSchemaBox
|
|
217
|
+
import { s, schema, createSchemaBox } from "cogsbox-shape";
|
|
218
|
+
import { z } from "zod";
|
|
211
219
|
|
|
212
220
|
const users = schema({
|
|
213
221
|
_tableName: "users",
|
|
214
|
-
id: s
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
id: s.sql({ type: "int", pk: true }).initialState({
|
|
223
|
+
value: () => `user_${crypto.randomUUID()}`,
|
|
224
|
+
schema: z.string(),
|
|
225
|
+
}),
|
|
217
226
|
|
|
218
227
|
email: s
|
|
219
228
|
.sql({ type: "varchar", length: 255 })
|
|
220
|
-
.
|
|
229
|
+
.server(({ sql }) => sql.email()),
|
|
221
230
|
|
|
222
231
|
metadata: s
|
|
223
232
|
.sql({ type: "text" })
|
|
@@ -227,14 +236,14 @@ const users = schema({
|
|
|
227
236
|
theme: z.enum(["light", "dark"]),
|
|
228
237
|
notifications: z.boolean(),
|
|
229
238
|
}),
|
|
230
|
-
})
|
|
239
|
+
}),
|
|
231
240
|
)
|
|
232
241
|
.transform({
|
|
233
242
|
toClient: (json) => JSON.parse(json),
|
|
234
243
|
toDb: (obj) => JSON.stringify(obj),
|
|
235
244
|
}),
|
|
236
245
|
|
|
237
|
-
posts: s.hasMany({
|
|
246
|
+
posts: s.hasMany({ count: 0 }), // Default to an empty array
|
|
238
247
|
});
|
|
239
248
|
|
|
240
249
|
const posts = schema({
|
|
@@ -257,58 +266,38 @@ const box = createSchemaBox({ users, posts }, (s) => ({
|
|
|
257
266
|
},
|
|
258
267
|
}));
|
|
259
268
|
|
|
260
|
-
//
|
|
269
|
+
// 1. Create a View for your API response
|
|
261
270
|
const userApiView = box.users.createView({ posts: true });
|
|
271
|
+
const { client, server } = userApiView.schemas;
|
|
272
|
+
const { toClient, toDb } = userApiView.transforms;
|
|
262
273
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
userApiView;
|
|
266
|
-
type UserApiResponse = z.infer<typeof clientSchema>;
|
|
267
|
-
// {
|
|
268
|
-
// id: string | number;
|
|
269
|
-
// email: string;
|
|
270
|
-
// metadata: { preferences: { theme: 'light' | 'dark'; notifications: boolean; } };
|
|
271
|
-
// posts: { id: number; title: string; published: boolean; authorId: number | string; }[];
|
|
272
|
-
// }
|
|
273
|
-
|
|
274
|
-
// Create a new user with view-aware defaults
|
|
275
|
-
const newUser = defaults;
|
|
276
|
-
// newUser.posts is now guaranteed to be an empty array.
|
|
277
|
-
|
|
278
|
-
// Validate user input against the view's validation schema
|
|
279
|
-
const validated = validationSchema.parse(userInput);
|
|
274
|
+
// 2. Type Inference
|
|
275
|
+
type UserApiResponse = z.infer<typeof client>;
|
|
280
276
|
|
|
281
|
-
//
|
|
282
|
-
|
|
277
|
+
// 3. Validation
|
|
278
|
+
// Validate user input against the view's server schema
|
|
279
|
+
const validated = server.parse(userInput);
|
|
283
280
|
|
|
284
|
-
//
|
|
285
|
-
const
|
|
281
|
+
// 4. Transformation
|
|
282
|
+
const dbUser = toDb(validated); // Ready for SQL
|
|
283
|
+
const apiUser = toClient(dbUser); // Ready for API response
|
|
286
284
|
```
|
|
287
285
|
|
|
288
|
-
## Why This Approach?
|
|
289
|
-
|
|
290
|
-
1. **Type Safety**: Full TypeScript support with inferred types at every layer.
|
|
291
|
-
2. **Single Source of Truth**: Define your schema once, use it everywhere.
|
|
292
|
-
3. **Explicit Data Loading**: Views encourage explicitly defining the data shape you need, preventing over-fetching.
|
|
293
|
-
4. **Transformation Co-location**: Keep data transformations next to field definitions.
|
|
294
|
-
5. **Progressive Enhancement**: Start simple, add complexity as needed.
|
|
295
|
-
6. **Framework Agnostic**: Works with any TypeScript project.
|
|
296
|
-
|
|
297
286
|
## API Reference
|
|
298
287
|
|
|
299
288
|
### Schema Definition
|
|
300
289
|
|
|
301
290
|
- `s.sql(config)`: Define SQL column type.
|
|
302
|
-
- `.initialState(value)`: Set default value for new records.
|
|
303
|
-
- `.client(schema)`: Define client-side schema.
|
|
304
|
-
- `.
|
|
305
|
-
- `.transform(transforms)`: Define
|
|
291
|
+
- `.initialState({ value, schema })`: Set default value for new records and the Zod schema for that state.
|
|
292
|
+
- `.client(schema | fn)`: Define client-side schema.
|
|
293
|
+
- `.server(schema | fn)`: Add validation rules (runs on server before DB insertion).
|
|
294
|
+
- `.transform(transforms)`: Define `toClient` and `toDb` transformations.
|
|
306
295
|
|
|
307
296
|
### Relationships
|
|
308
297
|
|
|
309
298
|
- `s.reference(getter)`: Create a foreign key reference.
|
|
310
299
|
- `s.hasMany(config)`: Define one-to-many relationship placeholder.
|
|
311
|
-
- `s.hasOne()`: Define one-to-one relationship placeholder.
|
|
300
|
+
- `s.hasOne(config)`: Define one-to-one relationship placeholder.
|
|
312
301
|
- `s.manyToMany(config)`: Define many-to-many relationship placeholder.
|
|
313
302
|
|
|
314
303
|
### Schema Processing
|
|
@@ -316,10 +305,9 @@ const apiUser = toClient(dbUser);
|
|
|
316
305
|
- `schema(definition)`: Create a schema definition.
|
|
317
306
|
- `createSchemaBox(schemas, resolver)`: The main function to create and resolve a schema registry.
|
|
318
307
|
- From the box entry (e.g., `box.users`):
|
|
319
|
-
- `.schemas`: Access base Zod schemas (
|
|
308
|
+
- `.schemas`: Access base Zod schemas (excludes relations).
|
|
320
309
|
- `.defaults`: Access base default values.
|
|
321
|
-
- `.
|
|
322
|
-
- `.createView(selection)`: Creates a new set of schemas and transforms including the selected relations.
|
|
310
|
+
- `.createView(selection)`: Creates a specific view including selected relations.
|
|
323
311
|
|
|
324
312
|
## License
|
|
325
313
|
|
package/dist/schema.js
CHANGED
|
@@ -171,7 +171,10 @@ function createBuilder(config) {
|
|
|
171
171
|
const newCompletedStages = new Set(completedStages);
|
|
172
172
|
newCompletedStages.add("new");
|
|
173
173
|
// Create union of the SQL type and the new client type
|
|
174
|
-
const
|
|
174
|
+
const hasProvidedSchema = !!schemaOrModifier;
|
|
175
|
+
const clientAndServerSchema = hasProvidedSchema
|
|
176
|
+
? finalSchema
|
|
177
|
+
: z.union([config.sqlZod, finalSchema]);
|
|
175
178
|
const newConfig = { ...config.sqlConfig };
|
|
176
179
|
if (clientPk) {
|
|
177
180
|
// Add our metadata flag to the config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogsbox-shape",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.156",
|
|
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",
|