cogsbox-shape 0.5.109 → 0.5.111
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 +95 -46
- package/dist/schema.js +28 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,9 +121,11 @@ const userSchema = schema({
|
|
|
121
121
|
});
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
## Relationships
|
|
124
|
+
## Relationships and Views
|
|
125
125
|
|
|
126
|
-
Define relationships between schemas
|
|
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), //
|
|
145
|
+
authorId: s.reference(() => users.id), // Foreign key reference
|
|
144
146
|
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. Create the Registry (The "Box")
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 =
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
|
222
|
-
const
|
|
223
|
-
|
|
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:
|
|
228
|
-
// posts:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
282
|
+
const dbUser = toDb(validated);
|
|
239
283
|
|
|
240
284
|
// Transform for API response
|
|
241
|
-
const apiUser =
|
|
285
|
+
const apiUser = toClient(dbUser);
|
|
242
286
|
```
|
|
243
287
|
|
|
244
288
|
## Why This Approach?
|
|
245
289
|
|
|
246
|
-
1.
|
|
247
|
-
2.
|
|
248
|
-
3.
|
|
249
|
-
4.
|
|
250
|
-
5.
|
|
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)`:
|
|
273
|
-
-
|
|
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.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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
531
|
+
return z.object(primitiveShape);
|
|
543
532
|
}
|
|
544
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
569
|
-
client: buildView(tableName, "client", selection),
|
|
570
|
-
validation: buildView(tableName, "validation", selection),
|
|
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.
|
|
3
|
+
"version": "0.5.111",
|
|
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",
|