cogsbox-shape 0.5.102 → 0.5.104
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 +145 -104
- package/dist/schema.d.ts +591 -94
- package/dist/schema.js +203 -158
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ In full-stack applications, data flows through multiple layers:
|
|
|
23
23
|
|
|
24
24
|
- **Database** stores data in SQL types (integers, varchars, etc.)
|
|
25
25
|
- **Server** needs to transform data for clients (e.g., convert cents to dollars)
|
|
26
|
-
- **Client** expects data in specific formats (e.g., UUIDs
|
|
26
|
+
- **Client** expects data in specific formats (e.g., temporary UUIDs for new records)
|
|
27
27
|
- **Forms** need validation rules and default values
|
|
28
28
|
|
|
29
29
|
Traditional approaches require defining these transformations in multiple places, leading to type mismatches and runtime errors.
|
|
@@ -33,7 +33,9 @@ Traditional approaches require defining these transformations in multiple places
|
|
|
33
33
|
cogsbox-shape introduces a unified flow that mirrors how data moves through your application:
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
|
|
36
|
+
Initial State
|
|
37
|
+
\
|
|
38
|
+
SQL ←→ Transform ←→ Client ←→ Validation
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
This flow ensures type safety at every step while giving you control over transformations.
|
|
@@ -45,25 +47,25 @@ This flow ensures type safety at every step while giving you control over transf
|
|
|
45
47
|
Start with your database reality:
|
|
46
48
|
|
|
47
49
|
```typescript
|
|
50
|
+
import { s, schema } from "cogsbox-shape";
|
|
51
|
+
|
|
48
52
|
const userSchema = schema({
|
|
49
53
|
_tableName: "users",
|
|
50
|
-
id: s.
|
|
51
|
-
email: s.
|
|
52
|
-
createdAt: s.
|
|
54
|
+
id: s.sql({ type: "int", pk: true }), // In DB: integer auto-increment
|
|
55
|
+
email: s.sql({ type: "varchar", length: 255 }),
|
|
56
|
+
createdAt: s.sql({ type: "datetime", default: "CURRENT_TIMESTAMP" }),
|
|
53
57
|
});
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
### 2. Initial State - Define Creation Defaults
|
|
57
61
|
|
|
58
|
-
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:
|
|
59
63
|
|
|
60
64
|
```typescript
|
|
61
65
|
const userSchema = schema({
|
|
62
66
|
_tableName: "users",
|
|
63
|
-
id: s
|
|
64
|
-
|
|
65
|
-
.initialState(z.string().uuid(), () => crypto.randomUUID()),
|
|
66
|
-
// DB stores integers, but client can work with UUID strings for new records
|
|
67
|
+
id: s.sql({ type: "int", pk: true }).initialState(() => crypto.randomUUID()),
|
|
68
|
+
// DB stores integers, but new records start with UUID strings
|
|
67
69
|
// This automatically creates a union type: number | string on the client
|
|
68
70
|
});
|
|
69
71
|
```
|
|
@@ -75,14 +77,10 @@ Transform how data appears to clients:
|
|
|
75
77
|
```typescript
|
|
76
78
|
const productSchema = schema({
|
|
77
79
|
_tableName: "products",
|
|
78
|
-
id: s
|
|
79
|
-
.int({ pk: true })
|
|
80
|
-
.initialState(z.string(), () => `tmp_${Date.now()}`)
|
|
81
|
-
.client(({ sql, initialState }) => z.union([sql, initialState])),
|
|
82
|
-
// Client can receive either the integer (from DB) or string (when creating)
|
|
80
|
+
id: s.sql({ type: "int", pk: true }).initialState(() => `tmp_${Date.now()}`),
|
|
83
81
|
|
|
84
82
|
price: s
|
|
85
|
-
.int
|
|
83
|
+
.sql({ type: "int" }) // Stored as cents in DB
|
|
86
84
|
.client(() => z.number().multipleOf(0.01)) // But dollars on client
|
|
87
85
|
.transform({
|
|
88
86
|
toClient: (cents) => cents / 100,
|
|
@@ -93,143 +91,186 @@ const productSchema = schema({
|
|
|
93
91
|
|
|
94
92
|
### 4. Validation - Define Business Rules
|
|
95
93
|
|
|
96
|
-
Add validation that runs
|
|
94
|
+
Add validation that runs at your client -> server boundary:
|
|
97
95
|
|
|
98
96
|
```typescript
|
|
99
97
|
const userSchema = schema({
|
|
100
98
|
_tableName: "users",
|
|
101
99
|
email: s
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.validation(({ client }) => client.email().toLowerCase()),
|
|
100
|
+
.sql({ type: "varchar", length: 255 })
|
|
101
|
+
.validation(({ sql }) => sql.email().toLowerCase()),
|
|
105
102
|
|
|
106
|
-
age: s.int
|
|
103
|
+
age: s.sql({ type: "int" }).validation(({ sql }) => sql.min(18).max(120)),
|
|
107
104
|
});
|
|
108
105
|
```
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
The flow matches how data moves through your application:
|
|
107
|
+
### 5. Transform - Convert Between Representations
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
2. **Initial State**: What shape new records take before persistence
|
|
116
|
-
3. **Client**: How data looks in your UI/API
|
|
117
|
-
4. **Validation**: Business rules applied to user input
|
|
118
|
-
5. **Transform**: Convert between database and client representations
|
|
119
|
-
|
|
120
|
-
Each step can reference previous steps, creating a pipeline:
|
|
109
|
+
Define bidirectional transformations between database and client:
|
|
121
110
|
|
|
122
111
|
```typescript
|
|
123
|
-
const
|
|
124
|
-
_tableName: "
|
|
112
|
+
const userSchema = schema({
|
|
113
|
+
_tableName: "users",
|
|
125
114
|
status: s
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
.initialState(z.literal("draft"), () => "draft")
|
|
129
|
-
// 2. Initial: New orders start as 'draft'
|
|
130
|
-
.client(({ sql }) => z.enum(["draft", "pending", "shipped", "delivered"]))
|
|
131
|
-
// 3. Client: Enforce enum on client
|
|
132
|
-
.validation(({ client }) => client),
|
|
133
|
-
// 4. Validation: Use same rules as client
|
|
134
|
-
|
|
135
|
-
totalPrice: s
|
|
136
|
-
.int()
|
|
137
|
-
// 1. SQL: Store as cents (integer)
|
|
138
|
-
.client(() => z.number().multipleOf(0.01))
|
|
139
|
-
// 2. Client: Work with dollars (decimal)
|
|
115
|
+
.sql({ type: "int" }) // 0 or 1 in database
|
|
116
|
+
.client(() => z.enum(["active", "inactive"])) // String enum on client
|
|
140
117
|
.transform({
|
|
141
|
-
toClient: (
|
|
142
|
-
toDb: (
|
|
118
|
+
toClient: (dbValue) => (dbValue === 1 ? "active" : "inactive"),
|
|
119
|
+
toDb: (clientValue) => (clientValue === "active" ? 1 : 0),
|
|
143
120
|
}),
|
|
144
|
-
// 3. Transform: Automatically convert between cents and dollars
|
|
145
121
|
});
|
|
146
122
|
```
|
|
147
123
|
|
|
148
|
-
|
|
124
|
+
## Relationships with Schema Box Registry
|
|
125
|
+
|
|
126
|
+
Define relationships between schemas using the Schema Box Registry pattern:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { s, schema, createSchemaBox } from "cogsbox-shape";
|
|
130
|
+
|
|
131
|
+
// Define schemas with relationship placeholders
|
|
132
|
+
const users = schema({
|
|
133
|
+
_tableName: "users",
|
|
134
|
+
id: s.sql({ type: "int", pk: true }),
|
|
135
|
+
name: s.sql({ type: "varchar" }),
|
|
136
|
+
posts: s.hasMany(), // Placeholder for relationship
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const posts = schema({
|
|
140
|
+
_tableName: "posts",
|
|
141
|
+
id: s.sql({ type: "int", pk: true }),
|
|
142
|
+
title: s.sql({ type: "varchar" }),
|
|
143
|
+
authorId: s.reference(() => users.id), // Reference to user
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Create registry and resolve relationships
|
|
147
|
+
const schemas = createSchemaBox({ users, posts }, (s) => ({
|
|
148
|
+
users: {
|
|
149
|
+
posts: { fromKey: "id", toKey: s.posts.authorId },
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
// Use the schemas
|
|
154
|
+
const { zodSchemas } = schemas.users;
|
|
155
|
+
const { clientSchema, defaultValues, toClient, toDb } = zodSchemas;
|
|
156
|
+
|
|
157
|
+
// Type-safe operations
|
|
158
|
+
const newUser = defaultValues; // Fully typed with defaults
|
|
159
|
+
const dbUser = toDb(clientData); // Transform for database
|
|
160
|
+
const apiUser = toClient(dbData); // Transform for API
|
|
161
|
+
```
|
|
149
162
|
|
|
150
163
|
## Real-World Example
|
|
151
164
|
|
|
152
165
|
Here's a complete example showing the power of the flow:
|
|
153
166
|
|
|
154
167
|
```typescript
|
|
155
|
-
|
|
168
|
+
import { s, schema, createSchemaBox, z } from "cogsbox-shape";
|
|
169
|
+
|
|
170
|
+
const users = schema({
|
|
156
171
|
_tableName: "users",
|
|
157
|
-
id: s
|
|
172
|
+
id: s
|
|
173
|
+
.sql({ type: "int", pk: true })
|
|
174
|
+
.initialState(() => `user_${crypto.randomUUID()}`),
|
|
158
175
|
|
|
159
|
-
email: s
|
|
176
|
+
email: s
|
|
177
|
+
.sql({ type: "varchar", length: 255 })
|
|
178
|
+
.validation(({ sql }) => sql.email()),
|
|
160
179
|
|
|
161
180
|
metadata: s
|
|
162
|
-
.text
|
|
163
|
-
.initialState(
|
|
181
|
+
.sql({ type: "text" })
|
|
182
|
+
.initialState(() => ({
|
|
183
|
+
preferences: { theme: "light", notifications: true },
|
|
184
|
+
}))
|
|
185
|
+
.client(() =>
|
|
164
186
|
z.object({
|
|
165
187
|
preferences: z.object({
|
|
166
188
|
theme: z.enum(["light", "dark"]),
|
|
167
189
|
notifications: z.boolean(),
|
|
168
190
|
}),
|
|
169
|
-
})
|
|
170
|
-
() => ({ preferences: { theme: "light", notifications: true } })
|
|
191
|
+
})
|
|
171
192
|
)
|
|
172
|
-
.client(({ initialState }) => initialState)
|
|
173
193
|
.transform({
|
|
174
194
|
toClient: (json) => JSON.parse(json),
|
|
175
195
|
toDb: (obj) => JSON.stringify(obj),
|
|
176
196
|
}),
|
|
197
|
+
|
|
198
|
+
posts: s.hasMany({ defaultCount: 0 }),
|
|
177
199
|
});
|
|
178
200
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})
|
|
201
|
+
const posts = schema({
|
|
202
|
+
_tableName: "posts",
|
|
203
|
+
id: s.sql({ type: "int", pk: true }),
|
|
204
|
+
title: s.sql({ type: "varchar" }),
|
|
205
|
+
published: s
|
|
206
|
+
.sql({ type: "int" }) // 0 or 1 in DB
|
|
207
|
+
.client(() => z.boolean())
|
|
208
|
+
.transform({
|
|
209
|
+
toClient: (int) => Boolean(int),
|
|
210
|
+
toDb: (bool) => (bool ? 1 : 0),
|
|
211
|
+
}),
|
|
212
|
+
authorId: s.reference(() => users.id),
|
|
213
|
+
});
|
|
190
214
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
const schemas = createSchemaBox({ users, posts }, (s) => ({
|
|
216
|
+
users: {
|
|
217
|
+
posts: { fromKey: "id", toKey: s.posts.authorId },
|
|
218
|
+
},
|
|
219
|
+
}));
|
|
194
220
|
|
|
195
221
|
// Use in your app
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
const userSchemas = schemas.users.zodSchemas;
|
|
223
|
+
type User = z.infer<typeof userSchemas.clientSchema>;
|
|
224
|
+
// {
|
|
225
|
+
// id: string | number;
|
|
226
|
+
// email: string;
|
|
227
|
+
// metadata: { preferences: { theme: "light" | "dark"; notifications: boolean } };
|
|
228
|
+
// posts: Array<Post>;
|
|
229
|
+
// }
|
|
230
|
+
|
|
231
|
+
// Create new user with defaults
|
|
232
|
+
const newUser = userSchemas.defaultValues;
|
|
233
|
+
|
|
234
|
+
// Validate user input
|
|
235
|
+
const validated = userSchemas.validationSchema.parse(userInput);
|
|
236
|
+
|
|
237
|
+
// Transform for database
|
|
238
|
+
const dbUser = userSchemas.toDb(validated);
|
|
239
|
+
|
|
240
|
+
// Transform for API response
|
|
241
|
+
const apiUser = userSchemas.toClient(dbUser);
|
|
200
242
|
```
|
|
201
243
|
|
|
202
|
-
##
|
|
244
|
+
## Why This Approach?
|
|
203
245
|
|
|
204
|
-
|
|
246
|
+
1. **Type Safety**: Full TypeScript support with inferred types at every layer
|
|
247
|
+
2. **Single Source of Truth**: Define your schema once, use it everywhere
|
|
248
|
+
3. **Transformation Co-location**: Keep data transformations next to field definitions
|
|
249
|
+
4. **Progressive Enhancement**: Start simple, add complexity as needed
|
|
250
|
+
5. **Framework Agnostic**: Works with any TypeScript project
|
|
205
251
|
|
|
206
|
-
|
|
207
|
-
const messageSchema = schema({
|
|
208
|
-
_tableName: "messages",
|
|
209
|
-
id: s.int({ pk: true }).initialState(z.string(), () => uuidv4()),
|
|
210
|
-
content: s.text(),
|
|
211
|
-
timestamp: s.datetime(),
|
|
212
|
-
});
|
|
252
|
+
## API Reference
|
|
213
253
|
|
|
214
|
-
|
|
215
|
-
recipients: rel
|
|
216
|
-
.hasMany({
|
|
217
|
-
fromKey: "id",
|
|
218
|
-
toKey: () => recipientRelations.messageId,
|
|
219
|
-
})
|
|
220
|
-
.validation(({ sql }) => sql.min(1, "Must have at least one recipient")),
|
|
221
|
-
}));
|
|
254
|
+
### Schema Definition
|
|
222
255
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
256
|
+
- `s.sql(config)`: Define SQL column type
|
|
257
|
+
- `.initialState(value)`: Set default value for new records
|
|
258
|
+
- `.client(schema)`: Define client-side schema
|
|
259
|
+
- `.validation(schema)`: Add validation rules
|
|
260
|
+
- `.transform(transforms)`: Define bidirectional transformations
|
|
261
|
+
|
|
262
|
+
### Relationships
|
|
263
|
+
|
|
264
|
+
- `s.reference(getter)`: Create a foreign key reference
|
|
265
|
+
- `s.hasMany(config)`: Define one-to-many relationship
|
|
266
|
+
- `s.hasOne()`: Define one-to-one relationship
|
|
267
|
+
- `s.manyToMany(config)`: Define many-to-many relationship
|
|
268
|
+
|
|
269
|
+
### Schema Processing
|
|
270
|
+
|
|
271
|
+
- `schema(definition)`: Create a schema definition
|
|
272
|
+
- `createSchemaBox(schemas, resolver)`: Create and resolve schema relationships
|
|
273
|
+
- `createSchema(schema, relations?)`: Generate Zod schemas (legacy API)
|
|
233
274
|
|
|
234
275
|
## License
|
|
235
276
|
|