cogsbox-shape 0.5.110 → 0.5.112

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
@@ -121,9 +121,11 @@ const userSchema = schema({
121
121
  });
122
122
  ```
123
123
 
124
- ## Relationships with Schema Box Registry
124
+ ## Relationships and Views
125
125
 
126
- Define relationships between schemas using the Schema Box Registry pattern:
126
+ Define relationships between schemas and create specific data views using the `createSchemaBox`.
127
+
128
+ ### 1. Define Schemas with Placeholders
127
129
 
128
130
  ```typescript
129
131
  import { s, schema, createSchemaBox } from "cogsbox-shape";
@@ -133,31 +135,71 @@ const users = schema({
133
135
  _tableName: "users",
134
136
  id: s.sql({ type: "int", pk: true }),
135
137
  name: s.sql({ type: "varchar" }),
136
- posts: s.hasMany(), // Placeholder for relationship
138
+ posts: s.hasMany(), // Placeholder for a one-to-many relationship
137
139
  });
138
140
 
139
141
  const posts = schema({
140
142
  _tableName: "posts",
141
143
  id: s.sql({ type: "int", pk: true }),
142
144
  title: s.sql({ type: "varchar" }),
143
- authorId: s.reference(() => users.id), // Reference to user
145
+ authorId: s.reference(() => users.id), // Foreign key reference
144
146
  });
147
+ ```
148
+
149
+ ### 2. Create the Registry (The "Box")
145
150
 
146
- // Create registry and resolve relationships
147
- const schemas = createSchemaBox({ users, posts }, (s) => ({
151
+ The `createSchemaBox` function processes your raw schemas, resolves the relationships, and gives you a powerful, type-safe API for accessing them.
152
+
153
+ ```typescript
154
+ const box = createSchemaBox({ users, posts }, (s) => ({
148
155
  users: {
156
+ // Resolve the 'posts' relation on the 'users' schema
149
157
  posts: { fromKey: "id", toKey: s.posts.authorId },
150
158
  },
151
159
  }));
160
+ ```
152
161
 
153
- // Use the schemas
154
- const { zodSchemas } = schemas.users;
155
- const { clientSchema, defaultValues, toClient, toDb } = zodSchemas;
162
+ ### 3. Access Base Schemas and Defaults
163
+
164
+ Once the box is created, you can access the base schemas (without relations) and their default values.
165
+
166
+ ```typescript
167
+ // Access the processed schemas for the 'users' table
168
+ const userSchemas = box.users.schemas;
169
+ const userDefaults = box.users.defaults;
156
170
 
157
171
  // 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
172
+ const newUser = userDefaults; // { id: 0, name: '' }
173
+
174
+ // The base schema does NOT include the 'posts' relation
175
+ type UserClient = z.infer<typeof userSchemas.client>; // { id: number; name: string; }
176
+ ```
177
+
178
+ ### 4. Create Views to Include Relations
179
+
180
+ The real power is in creating views to select exactly which relationships to include for a given operation.
181
+
182
+ ```typescript
183
+ // Create a view that includes the 'posts' for a user
184
+ const userWithPostsView = box.users.createView({
185
+ posts: true, // Select the 'posts' relation
186
+ });
187
+
188
+ // The type of this view now includes the nested posts
189
+ type UserWithPosts = z.infer<typeof userWithPostsView.client>;
190
+ // {
191
+ // id: number;
192
+ // name: string;
193
+ // posts: {
194
+ // id: number;
195
+ // title: string;
196
+ // authorId: number;
197
+ // }[];
198
+ // }
199
+
200
+ // You can also get default values for the view
201
+ const newUserWithPosts = userWithPostsView.defaults;
202
+ // { id: 0, name: '', posts: [] }
161
203
  ```
162
204
 
163
205
  ## Real-World Example
@@ -179,10 +221,7 @@ const users = schema({
179
221
 
180
222
  metadata: s
181
223
  .sql({ type: "text" })
182
- .initialState(() => ({
183
- preferences: { theme: "light", notifications: true },
184
- }))
185
- .client(() =>
224
+ .client(
186
225
  z.object({
187
226
  preferences: z.object({
188
227
  theme: z.enum(["light", "dark"]),
@@ -195,7 +234,7 @@ const users = schema({
195
234
  toDb: (obj) => JSON.stringify(obj),
196
235
  }),
197
236
 
198
- posts: s.hasMany({ defaultCount: 0 }),
237
+ posts: s.hasMany({ defaultCount: 0 }), // Default to an empty array
199
238
  });
200
239
 
201
240
  const posts = schema({
@@ -212,65 +251,75 @@ const posts = schema({
212
251
  authorId: s.reference(() => users.id),
213
252
  });
214
253
 
215
- const schemas = createSchemaBox({ users, posts }, (s) => ({
254
+ const box = createSchemaBox({ users, posts }, (s) => ({
216
255
  users: {
217
256
  posts: { fromKey: "id", toKey: s.posts.authorId },
218
257
  },
219
258
  }));
220
259
 
221
- // Use in your app
222
- const userSchemas = schemas.users.zodSchemas;
223
- type User = z.infer<typeof userSchemas.clientSchema>;
260
+ // Use a view for our API response
261
+ const userApiView = box.users.createView({ posts: true });
262
+
263
+ // Use the schemas from the view
264
+ const { clientSchema, validationSchema, defaults, toClient, toDb } =
265
+ userApiView;
266
+ type UserApiResponse = z.infer<typeof clientSchema>;
224
267
  // {
225
268
  // id: string | number;
226
269
  // email: string;
227
- // metadata: { preferences: { theme: "light" | "dark"; notifications: boolean } };
228
- // posts: Array<Post>;
270
+ // metadata: { preferences: { theme: 'light' | 'dark'; notifications: boolean; } };
271
+ // posts: { id: number; title: string; published: boolean; authorId: number | string; }[];
229
272
  // }
230
273
 
231
- // Create new user with defaults
232
- const newUser = userSchemas.defaultValues;
274
+ // Create a new user with view-aware defaults
275
+ const newUser = defaults;
276
+ // newUser.posts is now guaranteed to be an empty array.
233
277
 
234
- // Validate user input
235
- const validated = userSchemas.validationSchema.parse(userInput);
278
+ // Validate user input against the view's validation schema
279
+ const validated = validationSchema.parse(userInput);
236
280
 
237
281
  // Transform for database
238
- const dbUser = userSchemas.toDb(validated);
282
+ const dbUser = toDb(validated);
239
283
 
240
284
  // Transform for API response
241
- const apiUser = userSchemas.toClient(dbUser);
285
+ const apiUser = toClient(dbUser);
242
286
  ```
243
287
 
244
288
  ## Why This Approach?
245
289
 
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
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.
251
296
 
252
297
  ## API Reference
253
298
 
254
299
  ### Schema Definition
255
300
 
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
301
+ - `s.sql(config)`: Define SQL column type.
302
+ - `.initialState(value)`: Set default value for new records.
303
+ - `.client(schema)`: Define client-side schema.
304
+ - `.validation(schema)`: Add validation rules.
305
+ - `.transform(transforms)`: Define bidirectional transformations.
261
306
 
262
307
  ### Relationships
263
308
 
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
309
+ - `s.reference(getter)`: Create a foreign key reference.
310
+ - `s.hasMany(config)`: Define one-to-many relationship placeholder.
311
+ - `s.hasOne()`: Define one-to-one relationship placeholder.
312
+ - `s.manyToMany(config)`: Define many-to-many relationship placeholder.
268
313
 
269
314
  ### Schema Processing
270
315
 
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)
316
+ - `schema(definition)`: Create a schema definition.
317
+ - `createSchemaBox(schemas, resolver)`: The main function to create and resolve a schema registry.
318
+ - From the box entry (e.g., `box.users`):
319
+ - `.schemas`: Access base Zod schemas (sql, client, validation).
320
+ - `.defaults`: Access base default values.
321
+ - `.transforms`: Access `toClient` and `toDb` functions for the base schema.
322
+ - `.createView(selection)`: Creates a new set of schemas and transforms including the selected relations.
274
323
 
275
324
  ## License
276
325
 
package/dist/schema.d.ts CHANGED
@@ -306,6 +306,25 @@ type RegistryShape = Record<string, {
306
306
  toDb: (clientObject: any) => any;
307
307
  };
308
308
  }>;
309
+ type DeriveViewDefaults<TTableName extends keyof TRegistry, TSelection, TRegistry extends RegistryShape, Depth extends any[] = []> = Depth["length"] extends 10 ? any : // 1. Start with the base defaults for the table (id, name, etc.)
310
+ DeriveDefaults<TRegistry[TTableName]["rawSchema"]> & (TSelection extends Record<string, any> ? {
311
+ -readonly [K in keyof TSelection & keyof TRegistry[TTableName]["rawSchema"]]?: TRegistry[TTableName]["rawSchema"][K] extends {
312
+ config: {
313
+ sql: {
314
+ type: infer RelType;
315
+ schema: () => infer S;
316
+ };
317
+ };
318
+ } ? S extends {
319
+ _tableName: infer Target;
320
+ } ? Target extends keyof TRegistry ? RelType extends "hasMany" | "manyToMany" ? DeriveViewDefaults<Target, TSelection[K], TRegistry, [
321
+ ...Depth,
322
+ 1
323
+ ]>[] : DeriveViewDefaults<Target, TSelection[K], TRegistry, [
324
+ ...Depth,
325
+ 1
326
+ ]> | null | undefined : never : never : never;
327
+ } : {});
309
328
  type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R extends ResolutionMap<S>, Resolved extends RegistryShape = ResolvedRegistryWithSchemas<S, R> extends RegistryShape ? ResolvedRegistryWithSchemas<S, R> : RegistryShape> = {
310
329
  [K in keyof Resolved]: {
311
330
  definition: Resolved[K]["rawSchema"];
@@ -325,7 +344,7 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
325
344
  sql: Resolved[K]["zodSchemas"]["sqlSchema"];
326
345
  client: z.ZodObject<BuildZodShape<K, TSelection, "clientSchema", Resolved>>;
327
346
  validation: z.ZodObject<BuildZodShape<K, TSelection, "validationSchema", Resolved>>;
328
- defaults: any;
347
+ defaults: Prettify<DeriveViewDefaults<K & string, TSelection, Resolved>>;
329
348
  };
330
349
  };
331
350
  };
package/dist/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { z, ZodType } from "zod";
2
2
  import { ca } from "zod/v4/locales";
3
3
  export const isFunction = (fn) => typeof fn === "function";
4
4
  // Function to create a properly typed current timestamp config
@@ -301,10 +301,8 @@ function inferDefaultFromZod(zodType, sqlConfig) {
301
301
  if (sqlConfig.default === "CURRENT_TIMESTAMP") {
302
302
  return undefined;
303
303
  }
304
- // Otherwise, use the provided SQL default.
305
304
  return sqlConfig.default;
306
305
  }
307
- // --- PRESERVED LOGIC: Handle relation types (NO CHANGES HERE) ---
308
306
  if (typeof sqlConfig.type === "string" &&
309
307
  ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
310
308
  const relationConfig = sqlConfig;
@@ -317,7 +315,6 @@ function inferDefaultFromZod(zodType, sqlConfig) {
317
315
  return {};
318
316
  }
319
317
  }
320
- // --- PRESERVED LOGIC: Handle basic SQL types as a fallback (NO CHANGES HERE) ---
321
318
  const sqlTypeConfig = sqlConfig;
322
319
  if (sqlTypeConfig.type && !sqlTypeConfig.nullable) {
323
320
  switch (sqlTypeConfig.type) {
@@ -339,19 +336,16 @@ function inferDefaultFromZod(zodType, sqlConfig) {
339
336
  return null;
340
337
  }
341
338
  }
342
- // --- PRESERVED LOGIC: Fall back to Zod-based inference ---
343
- if (zodType instanceof z.ZodOptional) {
344
- return undefined;
345
- }
346
- if (zodType instanceof z.ZodDefault && zodType._def?.defaultValue) {
347
- return typeof zodType._def.defaultValue === "function"
348
- ? zodType._def.defaultValue()
349
- : zodType._def.defaultValue;
339
+ if ("_def" in zodType && "defaultValue" in zodType._def) {
340
+ const def = zodType._def;
341
+ const val = def.defaultValue;
342
+ if (val !== undefined) {
343
+ return typeof val === "function" ? val() : val;
344
+ }
350
345
  }
351
346
  if (zodType instanceof z.ZodString) {
352
347
  return "";
353
348
  }
354
- // Return undefined if no other default can be determined.
355
349
  return undefined;
356
350
  }
357
351
  export function createMixedValidationSchema(schema, clientSchema, dbSchema) {
@@ -528,30 +522,26 @@ function createViewObject(tableName, selection, registry) {
528
522
  const registryEntry = registry[currentTable];
529
523
  const rawSchema = registryEntry.rawSchema;
530
524
  const baseSchema = registryEntry.zodSchemas[`${schemaType}Schema`];
531
- // Get all relation keys
532
- const relationsToOmit = {};
533
- for (const key in rawSchema) {
534
- if (rawSchema[key]?.config?.sql?.schema) {
535
- relationsToOmit[key] = true;
536
- }
537
- }
538
- // Start with primitive fields only
539
- const schemaWithPrimitives = baseSchema.omit(relationsToOmit);
540
- // If selection is just `true`, return primitives only (no nested relations)
525
+ // --- START OF THE FIX ---
526
+ // 1. Get the shape of the base schema (e.g., { id: z.ZodNumber, name: z.ZodString })
527
+ // The base schema correctly contains only primitive/referenced fields.
528
+ const primitiveShape = baseSchema.shape;
529
+ // 2. If the selection is just `true`, we don't need to add any relations.
541
530
  if (subSelection === true) {
542
- return schemaWithPrimitives;
531
+ return z.object(primitiveShape);
543
532
  }
544
- // Otherwise, add selected relations
533
+ // 3. Build a new shape object for the selected relations.
545
534
  const selectedRelationShapes = {};
546
535
  if (typeof subSelection === "object") {
547
536
  for (const key in subSelection) {
548
- if (subSelection[key] && relationsToOmit[key]) {
537
+ // We only care about keys that are actual relations in the raw schema.
538
+ if (subSelection[key] && rawSchema[key]?.config?.sql?.schema) {
549
539
  const relationConfig = rawSchema[key].config.sql;
550
540
  const targetTable = relationConfig.schema()._tableName;
551
- // Recursively build the sub-schema
541
+ // Recursively build the sub-schema for the relation.
552
542
  const subSchema = buildView(targetTable, schemaType, subSelection[key]);
553
- if (relationConfig.type === "hasMany" ||
554
- relationConfig.type === "manyToMany") {
543
+ // Wrap it in an array or optional as needed.
544
+ if (["hasMany", "manyToMany"].includes(relationConfig.type)) {
555
545
  selectedRelationShapes[key] = z.array(subSchema);
556
546
  }
557
547
  else {
@@ -560,14 +550,18 @@ function createViewObject(tableName, selection, registry) {
560
550
  }
561
551
  }
562
552
  }
563
- return schemaWithPrimitives.extend(selectedRelationShapes);
553
+ // 4. Combine the primitive shape and the new relation shapes into one final shape.
554
+ const finalShape = { ...primitiveShape, ...selectedRelationShapes };
555
+ // 5. Return a brand new, clean Zod object from the final shape.
556
+ return z.object(finalShape);
557
+ // --- END OF THE FIX ---
564
558
  }
565
- // The main function builds the final object with all three schemas.
559
+ // The main function builds the final object with both schemas.
566
560
  const sourceRegistryEntry = registry[tableName];
567
561
  return {
568
- sql: sourceRegistryEntry.zodSchemas.sqlSchema, // Changed from sqlSchema
569
- client: buildView(tableName, "client", selection), // Changed from clientSchema
570
- validation: buildView(tableName, "validation", selection), // Changed from validationSchema
562
+ sql: sourceRegistryEntry.zodSchemas.sqlSchema,
563
+ client: buildView(tableName, "client", selection),
564
+ validation: buildView(tableName, "validation", selection),
571
565
  };
572
566
  }
573
567
  export function createSchemaBox(schemas, resolver) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.110",
3
+ "version": "0.5.112",
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",