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 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,186 @@ 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
+ }));
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
- const userSchema = schema({
168
+ import { s, schema, createSchemaBox, z } from "cogsbox-shape";
169
+
170
+ const users = schema({
156
171
  _tableName: "users",
157
- id: s.int({ pk: true }).initialState(z.string().uuid(), () => uuidv4()),
172
+ id: s
173
+ .sql({ type: "int", pk: true })
174
+ .initialState(() => `user_${crypto.randomUUID()}`),
158
175
 
159
- email: s.varchar({ length: 255 }).validation(({ sql }) => sql.email()),
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 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
- }));
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
- // Generate schemas
192
- const { sqlSchema, clientSchema, validationSchema, defaultValues } =
193
- createSchema(userSchema, userRelations);
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 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
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
- ## Relationships
244
+ ## Why This Approach?
203
245
 
204
- Define relationships that are type-safe across all layers:
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
- ```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
- });
252
+ ## API Reference
213
253
 
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
- }));
254
+ ### Schema Definition
222
255
 
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
- ```
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