cogsbox-shape 0.5.102 → 0.5.103

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 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 as strings, not numbers)
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
- SQL → Initial State → Client → Validation
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.int({ pk: true }), // In DB: integer auto-increment
51
- email: s.varchar({ length: 255 }),
52
- createdAt: s.datetime({ default: "CURRENT_TIMESTAMP" }),
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. Initial state serves two purposes: defining default values AND adding additional types to the client schema.
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
- .int({ pk: true })
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() // Stored as cents in DB
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,187 @@ const productSchema = schema({
93
91
 
94
92
  ### 4. Validation - Define Business Rules
95
93
 
96
- Add validation that runs before data enters your system:
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
- .varchar({ length: 255 })
103
- .client(({ sql }) => sql)
104
- .validation(({ client }) => client.email().toLowerCase()),
100
+ .sql({ type: "varchar", length: 255 })
101
+ .validation(({ sql }) => sql.email().toLowerCase()),
105
102
 
106
- age: s.int().validation(({ sql }) => sql.min(18).max(120)),
103
+ age: s.sql({ type: "int" }).validation(({ sql }) => sql.min(18).max(120)),
107
104
  });
108
105
  ```
109
106
 
110
- ## Why This Flow?
111
-
112
- The flow matches how data moves through your application:
107
+ ### 5. Transform - Convert Between Representations
113
108
 
114
- 1. **SQL**: Database constraints and types
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 orderSchema = schema({
124
- _tableName: "orders",
112
+ const userSchema = schema({
113
+ _tableName: "users",
125
114
  status: s
126
- .varchar({ length: 20 })
127
- // 1. SQL: Simple varchar in database
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: (cents) => cents / 100,
142
- toDb: (dollars) => Math.round(dollars * 100),
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
- This approach ensures type safety throughout your entire data lifecycle while keeping transformations co-located with your schema definition.
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
+ posts: {}, // No outgoing relationships
152
+ }));
153
+
154
+ // Use the schemas
155
+ const { zodSchemas } = schemas.users;
156
+ const { clientSchema, defaultValues, toClient, toDb } = zodSchemas;
157
+
158
+ // Type-safe operations
159
+ const newUser = defaultValues; // Fully typed with defaults
160
+ const dbUser = toDb(clientData); // Transform for database
161
+ const apiUser = toClient(dbData); // Transform for API
162
+ ```
149
163
 
150
164
  ## Real-World Example
151
165
 
152
166
  Here's a complete example showing the power of the flow:
153
167
 
154
168
  ```typescript
155
- const userSchema = schema({
169
+ import { s, schema, createSchemaBox, z } from "cogsbox-shape";
170
+
171
+ const users = schema({
156
172
  _tableName: "users",
157
- id: s.int({ pk: true }).initialState(z.string().uuid(), () => uuidv4()),
173
+ id: s
174
+ .sql({ type: "int", pk: true })
175
+ .initialState(() => `user_${crypto.randomUUID()}`),
158
176
 
159
- email: s.varchar({ length: 255 }).validation(({ sql }) => sql.email()),
177
+ email: s
178
+ .sql({ type: "varchar", length: 255 })
179
+ .validation(({ sql }) => sql.email()),
160
180
 
161
181
  metadata: s
162
- .text()
163
- .initialState(
182
+ .sql({ type: "text" })
183
+ .initialState(() => ({
184
+ preferences: { theme: "light", notifications: true },
185
+ }))
186
+ .client(() =>
164
187
  z.object({
165
188
  preferences: z.object({
166
189
  theme: z.enum(["light", "dark"]),
167
190
  notifications: z.boolean(),
168
191
  }),
169
- }),
170
- () => ({ preferences: { theme: "light", notifications: true } })
192
+ })
171
193
  )
172
- .client(({ initialState }) => initialState)
173
194
  .transform({
174
195
  toClient: (json) => JSON.parse(json),
175
196
  toDb: (obj) => JSON.stringify(obj),
176
197
  }),
198
+
199
+ posts: s.hasMany({ defaultCount: 0 }),
177
200
  });
178
201
 
179
- const userRelations = schemaRelations(userSchema, (rel) => ({
180
- posts: rel
181
- .hasMany({
182
- fromKey: "id",
183
- toKey: () => postRelations.userId,
184
- defaultCount: 0,
185
- })
186
- .validation(({ client }) =>
187
- client.min(1, "User must have at least one post")
188
- ),
189
- }));
202
+ const posts = schema({
203
+ _tableName: "posts",
204
+ id: s.sql({ type: "int", pk: true }),
205
+ title: s.sql({ type: "varchar" }),
206
+ published: s
207
+ .sql({ type: "int" }) // 0 or 1 in DB
208
+ .client(() => z.boolean())
209
+ .transform({
210
+ toClient: (int) => Boolean(int),
211
+ toDb: (bool) => (bool ? 1 : 0),
212
+ }),
213
+ authorId: s.reference(() => users.id),
214
+ });
190
215
 
191
- // Generate schemas
192
- const { sqlSchema, clientSchema, validationSchema, defaultValues } =
193
- createSchema(userSchema, userRelations);
216
+ const schemas = createSchemaBox({ users, posts }, (s) => ({
217
+ users: {
218
+ posts: { fromKey: "id", toKey: s.posts.authorId },
219
+ },
220
+ }));
194
221
 
195
222
  // Use in your app
196
- const newUser = defaultValues; // Fully typed with defaults
197
- const validated = validationSchema.parse(userInput); // Runtime validation
198
- const dbUser = toDb(validated); // Transform for database
199
- const apiUser = toClient(dbUser); // Transform for API
223
+ const userSchemas = schemas.users.zodSchemas;
224
+ type User = z.infer<typeof userSchemas.clientSchema>;
225
+ // {
226
+ // id: string | number;
227
+ // email: string;
228
+ // metadata: { preferences: { theme: "light" | "dark"; notifications: boolean } };
229
+ // posts: Array<Post>;
230
+ // }
231
+
232
+ // Create new user with defaults
233
+ const newUser = userSchemas.defaultValues;
234
+
235
+ // Validate user input
236
+ const validated = userSchemas.validationSchema.parse(userInput);
237
+
238
+ // Transform for database
239
+ const dbUser = userSchemas.toDb(validated);
240
+
241
+ // Transform for API response
242
+ const apiUser = userSchemas.toClient(dbUser);
200
243
  ```
201
244
 
202
- ## Relationships
245
+ ## Why This Approach?
203
246
 
204
- Define relationships that are type-safe across all layers:
247
+ 1. **Type Safety**: Full TypeScript support with inferred types at every layer
248
+ 2. **Single Source of Truth**: Define your schema once, use it everywhere
249
+ 3. **Transformation Co-location**: Keep data transformations next to field definitions
250
+ 4. **Progressive Enhancement**: Start simple, add complexity as needed
251
+ 5. **Framework Agnostic**: Works with any TypeScript project
205
252
 
206
- ```typescript
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
- });
253
+ ## API Reference
213
254
 
214
- const messageRelations = schemaRelations(messageSchema, (rel) => ({
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
- }));
255
+ ### Schema Definition
222
256
 
223
- // The flow works with relationships too!
224
- const { clientSchema } = createSchema(messageSchema, messageRelations);
225
- type Message = z.infer<typeof clientSchema>;
226
- // {
227
- // id: string | number;
228
- // content: string;
229
- // timestamp: Date;
230
- // recipients: Array<Recipient>;
231
- // }
232
- ```
257
+ - `s.sql(config)`: Define SQL column type
258
+ - `.initialState(value)`: Set default value for new records
259
+ - `.client(schema)`: Define client-side schema
260
+ - `.validation(schema)`: Add validation rules
261
+ - `.transform(transforms)`: Define bidirectional transformations
262
+
263
+ ### Relationships
264
+
265
+ - `s.reference(getter)`: Create a foreign key reference
266
+ - `s.hasMany(config)`: Define one-to-many relationship
267
+ - `s.hasOne()`: Define one-to-one relationship
268
+ - `s.manyToMany(config)`: Define many-to-many relationship
269
+
270
+ ### Schema Processing
271
+
272
+ - `schema(definition)`: Create a schema definition
273
+ - `createSchemaBox(schemas, resolver)`: Create and resolve schema relationships
274
+ - `createSchema(schema, relations?)`: Generate Zod schemas (legacy API)
233
275
 
234
276
  ## License
235
277