cogsbox-shape 0.5.173 → 0.5.175
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 +21 -105
- package/dist/schema.d.ts +11 -22
- package/dist/schema.js +87 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,13 +36,13 @@ Define a field by chaining methods. Each step is optional — use only what you
|
|
|
36
36
|
s.sql() → .initialState() → .client() → .server() → .transform()
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
| Method
|
|
40
|
-
|
|
|
41
|
-
| `s.sql({ type })`
|
|
42
|
-
| `.initialState({ value, schema })` | Default value and type for new records created on the client. |
|
|
43
|
-
| `.client(fn)`
|
|
44
|
-
| `.server(fn)`
|
|
45
|
-
| `.transform({ toClient, toDb })`
|
|
39
|
+
| Method | Purpose |
|
|
40
|
+
| -------------------------------------------- | -------------------------------------------------------------- |
|
|
41
|
+
| `s.sql({ type })` | Database column type. The starting point for every field. |
|
|
42
|
+
| `.initialState({ value, schema, clientPk })` | Default value and type for new records created on the client. |
|
|
43
|
+
| `.client(fn)` | Client-side validation. Overrides the client type if needed. |
|
|
44
|
+
| `.server(fn)` | Server-side validation. Stricter rules before database writes. |
|
|
45
|
+
| `.transform({ toClient, toDb })` | Converts between database and client representations. |
|
|
46
46
|
|
|
47
47
|
### 1. SQL — Define Your Database Schema
|
|
48
48
|
|
|
@@ -72,6 +72,7 @@ const userSchema = schema({
|
|
|
72
72
|
id: s.sql({ type: "int", pk: true }).initialState({
|
|
73
73
|
value: () => crypto.randomUUID(),
|
|
74
74
|
schema: z.string(),
|
|
75
|
+
clientPk: true, // Explicitly marks this as a client PK, auto-creating a union type
|
|
75
76
|
}),
|
|
76
77
|
// Client type becomes: string | number (union of SQL + initialState)
|
|
77
78
|
// Default value: a generated UUID string
|
|
@@ -175,6 +176,7 @@ const contactSchema = schema({
|
|
|
175
176
|
id: s.sql({ type: "int", pk: true }).initialState({
|
|
176
177
|
value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
|
|
177
178
|
schema: z.string(),
|
|
179
|
+
clientPk: true,
|
|
178
180
|
}),
|
|
179
181
|
name: s.sql({ type: "varchar" }).server(({ sql }) => sql.min(2)),
|
|
180
182
|
email: s.sql({ type: "varchar" }).server(({ sql }) => sql.email()),
|
|
@@ -189,23 +191,24 @@ const contactSchema = schema({
|
|
|
189
191
|
|
|
190
192
|
const {
|
|
191
193
|
clientSchema, // Zod schema for client-side validation
|
|
192
|
-
|
|
194
|
+
serverSchema, // Zod schema with .server() rules
|
|
193
195
|
sqlSchema, // Zod schema matching DB column types
|
|
194
196
|
defaultValues, // Typed defaults matching clientSchema
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
generateDefaults, // Generates fresh defaults (executes randomizers/dates)
|
|
198
|
+
parseForDb, // Validates client app data & transforms to DB format
|
|
199
|
+
parseFromDb, // Transforms DB data & validates to Client format
|
|
197
200
|
} = createSchema(contactSchema);
|
|
198
201
|
|
|
199
202
|
// Use in a form
|
|
200
|
-
const [data, setData] = useState(
|
|
203
|
+
const [data, setData] = useState(generateDefaults());
|
|
201
204
|
// { id: "new_a1b2c3d4", name: "", email: "", isArchived: false }
|
|
202
205
|
|
|
203
|
-
// Validate
|
|
204
|
-
const result =
|
|
206
|
+
// Validate explicitly
|
|
207
|
+
const result = serverSchema.safeParse(data);
|
|
205
208
|
|
|
206
|
-
//
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
+
// Or handle validation & transformation in a single step!
|
|
210
|
+
const safeDbRow = parseForDb(data);
|
|
211
|
+
// Validates using serverSchema, outputs { isArchived: 0, ... }
|
|
209
212
|
```
|
|
210
213
|
|
|
211
214
|
## Relationships and Views
|
|
@@ -258,7 +261,7 @@ type UserClient = z.infer<typeof schemas.client>;
|
|
|
258
261
|
|
|
259
262
|
### 4. Create Views to Include Relations
|
|
260
263
|
|
|
261
|
-
Explicitly select which relations to include
|
|
264
|
+
Explicitly select which relations to include. The resulting views automatically apply nested transforms and deep schema validations.
|
|
262
265
|
|
|
263
266
|
```typescript
|
|
264
267
|
const userWithPosts = box.users.createView({
|
|
@@ -273,92 +276,5 @@ type UserWithPosts = z.infer<typeof userWithPosts.schemas.client>;
|
|
|
273
276
|
// }
|
|
274
277
|
|
|
275
278
|
const defaults = userWithPosts.defaults;
|
|
276
|
-
// { id: 0, name: '', posts:
|
|
279
|
+
// { id: 0, name: '', posts:
|
|
277
280
|
```
|
|
278
|
-
|
|
279
|
-
## Complete Example
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
import { s, schema, createSchemaBox } from "cogsbox-shape";
|
|
283
|
-
import { z } from "zod";
|
|
284
|
-
|
|
285
|
-
const users = schema({
|
|
286
|
-
_tableName: "users",
|
|
287
|
-
id: s.sql({ type: "int", pk: true }).initialState({
|
|
288
|
-
value: () => `user_${crypto.randomUUID()}`,
|
|
289
|
-
schema: z.string(),
|
|
290
|
-
}),
|
|
291
|
-
email: s
|
|
292
|
-
.sql({ type: "varchar", length: 255 })
|
|
293
|
-
.server(({ sql }) => sql.email()),
|
|
294
|
-
isActive: s
|
|
295
|
-
.sql({ type: "int" })
|
|
296
|
-
.client(() => z.boolean())
|
|
297
|
-
.transform({
|
|
298
|
-
toClient: (val) => val === 1,
|
|
299
|
-
toDb: (val) => (val ? 1 : 0),
|
|
300
|
-
}),
|
|
301
|
-
posts: s.hasMany({ count: 0 }),
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const posts = schema({
|
|
305
|
-
_tableName: "posts",
|
|
306
|
-
id: s.sql({ type: "int", pk: true }),
|
|
307
|
-
title: s.sql({ type: "varchar" }),
|
|
308
|
-
authorId: s.reference(() => users.id),
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const box = createSchemaBox({ users, posts }, (s) => ({
|
|
312
|
-
users: {
|
|
313
|
-
posts: { fromKey: "id", toKey: s.posts.authorId },
|
|
314
|
-
},
|
|
315
|
-
}));
|
|
316
|
-
|
|
317
|
-
// Base schema (no relations)
|
|
318
|
-
const { schemas, defaults, transforms } = box.users;
|
|
319
|
-
|
|
320
|
-
// View with relations
|
|
321
|
-
const userView = box.users.createView({ posts: true });
|
|
322
|
-
const { client, server } = userView.schemas;
|
|
323
|
-
|
|
324
|
-
// Type inference
|
|
325
|
-
type User = z.infer<typeof client>;
|
|
326
|
-
|
|
327
|
-
// Validation
|
|
328
|
-
const result = server.safeParse(formData);
|
|
329
|
-
|
|
330
|
-
// Transformation (server-side)
|
|
331
|
-
const dbRow = transforms.toDb(validated);
|
|
332
|
-
const apiResponse = transforms.toClient(dbRow);
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
## API Reference
|
|
336
|
-
|
|
337
|
-
### Schema Definition
|
|
338
|
-
|
|
339
|
-
- `s.sql(config)` — Define SQL column type. The starting point for every field.
|
|
340
|
-
- `.initialState({ value, schema })` — Set default value for new records. Optionally provide a Zod schema to widen or narrow the client type.
|
|
341
|
-
- `.client(schema | fn)` — Define the client-side Zod schema for validation. Use when the client type differs from SQL.
|
|
342
|
-
- `.server(schema | fn)` — Add server-side validation rules. Receives the previous schema in the chain for refinement.
|
|
343
|
-
- `.transform({ toClient, toDb })` — Define bidirectional conversion between SQL and client types. Runs on the server.
|
|
344
|
-
|
|
345
|
-
### Relationships
|
|
346
|
-
|
|
347
|
-
- `s.reference(getter)` — Create a foreign key reference to another schema's field.
|
|
348
|
-
- `s.hasMany(config)` — Declare a one-to-many relationship placeholder.
|
|
349
|
-
- `s.hasOne(config)` — Declare a one-to-one relationship placeholder.
|
|
350
|
-
- `s.manyToMany(config)` — Declare a many-to-many relationship placeholder.
|
|
351
|
-
|
|
352
|
-
### Schema Processing
|
|
353
|
-
|
|
354
|
-
- `schema(definition)` — Create a schema definition from field declarations.
|
|
355
|
-
- `createSchema(schema)` — Process a single schema. Returns `clientSchema`, `validationSchema`, `sqlSchema`, `defaultValues`, `toClient`, `toDb`.
|
|
356
|
-
- `createSchemaBox(schemas, resolver)` — Process multiple schemas with relationships. Returns a registry with:
|
|
357
|
-
- `.schemas` — Base Zod schemas (excludes relations).
|
|
358
|
-
- `.defaults` — Typed default values.
|
|
359
|
-
- `.transforms` — `toClient` and `toDb` functions.
|
|
360
|
-
- `.createView(selection)` — Create a view including selected relations.
|
|
361
|
-
|
|
362
|
-
## License
|
|
363
|
-
|
|
364
|
-
MIT
|
package/dist/schema.d.ts
CHANGED
|
@@ -47,21 +47,20 @@ export interface IBuilderMethods<T extends DbConfig, TSql extends z.ZodTypeAny,
|
|
|
47
47
|
uuid: () => string;
|
|
48
48
|
}) => TValue);
|
|
49
49
|
schema?: never;
|
|
50
|
-
clientPk?: boolean;
|
|
50
|
+
clientPk?: boolean | ((val: any) => boolean);
|
|
51
51
|
}): Prettify<Builder<"new", T, TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>, TValue extends () => infer R ? R : TValue, CollapsedUnion<TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>, CollapsedUnion<TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>>>;
|
|
52
52
|
initialState<const TSchema extends z.ZodTypeAny>(options: {
|
|
53
53
|
value?: never;
|
|
54
54
|
schema: TSchema;
|
|
55
|
-
clientPk?: boolean;
|
|
55
|
+
clientPk?: boolean | ((val: any) => boolean);
|
|
56
56
|
}): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>, CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
|
|
57
57
|
initialState<const TValue, const TSchema extends z.ZodTypeAny>(options: {
|
|
58
58
|
value: TValue | ((tools: {
|
|
59
59
|
uuid: () => string;
|
|
60
60
|
}) => TValue);
|
|
61
61
|
schema: TSchema | ((base: ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>) => TSchema);
|
|
62
|
-
clientPk?: boolean;
|
|
63
|
-
}): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>,
|
|
64
|
-
CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
|
|
62
|
+
clientPk?: boolean | ((val: any) => boolean);
|
|
63
|
+
}): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>, CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
|
|
65
64
|
reference: <TRefSchema extends {
|
|
66
65
|
_tableName: string;
|
|
67
66
|
}>(fieldGetter: () => any) => Builder<"sql", T & {
|
|
@@ -174,9 +173,7 @@ type PickPrimaryKeys<T extends ShapeSchema> = {
|
|
|
174
173
|
};
|
|
175
174
|
type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
|
|
176
175
|
__primaryKeySQL?: string;
|
|
177
|
-
__isClientChecker?: (record: any) => boolean;
|
|
178
176
|
primaryKeySQL: (definer: (pkFields: PickPrimaryKeys<T>) => string) => SchemaBuilder<T>;
|
|
179
|
-
isClient: (checker: (record: Prettify<z.infer<z.ZodObject<DeriveSchemaByKey<T, "zodSqlSchema">>> | z.infer<z.ZodObject<DeriveSchemaByKey<T, "zodClientSchema">>>>) => boolean) => SchemaBuilder<T>;
|
|
180
177
|
};
|
|
181
178
|
export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
|
|
182
179
|
export type RelationType = "hasMany" | "hasOne" | "manyToMany";
|
|
@@ -215,7 +212,7 @@ export declare function createSchema<T extends {
|
|
|
215
212
|
}, R extends Record<string, any> = {}, TActualSchema extends Omit<T & R, typeof SchemaWrapperBrand> = Omit<T & R, typeof SchemaWrapperBrand>>(schema: T, relations?: R): {
|
|
216
213
|
pk: string[] | null;
|
|
217
214
|
clientPk: string[] | null;
|
|
218
|
-
isClientRecord: (
|
|
215
|
+
isClientRecord: (record: any) => boolean;
|
|
219
216
|
sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
|
|
220
217
|
clientSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>;
|
|
221
218
|
serverSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>;
|
|
@@ -246,15 +243,7 @@ type KnownKeys<T> = keyof {
|
|
|
246
243
|
type ResolutionMap<S extends Record<string, SchemaWithPlaceholders>> = {
|
|
247
244
|
[TableName in keyof S]?: {
|
|
248
245
|
[FieldName in keyof S[TableName] as S[TableName][FieldName] extends PlaceholderReference | PlaceholderRelation<any> ? FieldName : never]?: S[TableName][FieldName] extends PlaceholderRelation<any> ? {
|
|
249
|
-
/**
|
|
250
|
-
* The key on the current table (`users`) to join from.
|
|
251
|
-
* Autocompletes with: 'id', 'name', etc.
|
|
252
|
-
*/
|
|
253
246
|
fromKey: KnownKeys<S[TableName]>;
|
|
254
|
-
/**
|
|
255
|
-
* The target key on the related table.
|
|
256
|
-
* Must be a field reference from the proxy, e.g., `s.pets.userId`
|
|
257
|
-
*/
|
|
258
247
|
toKey: {
|
|
259
248
|
__meta: any;
|
|
260
249
|
__parentTableType: any;
|
|
@@ -297,7 +286,7 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
|
|
|
297
286
|
parseFromDb: (dbData: any) => any;
|
|
298
287
|
pk: string[] | null;
|
|
299
288
|
clientPk: string[] | null;
|
|
300
|
-
isClientRecord: (
|
|
289
|
+
isClientRecord: (record: any) => boolean;
|
|
301
290
|
generateDefaults: () => Prettify<DeriveDefaults<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
|
|
302
291
|
};
|
|
303
292
|
};
|
|
@@ -414,7 +403,7 @@ type RegistryShape = Record<string, {
|
|
|
414
403
|
parseFromDb: (dbData: any) => any;
|
|
415
404
|
pk: string[] | null;
|
|
416
405
|
clientPk: string[] | null;
|
|
417
|
-
isClientRecord: (
|
|
406
|
+
isClientRecord: (record: any) => boolean;
|
|
418
407
|
generateDefaults: () => any;
|
|
419
408
|
};
|
|
420
409
|
}>;
|
|
@@ -438,7 +427,7 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
|
|
|
438
427
|
generateDefaults: () => Resolved[K]["zodSchemas"]["defaultValues"];
|
|
439
428
|
pk: string[] | null;
|
|
440
429
|
clientPk: string[] | null;
|
|
441
|
-
isClientRecord: (
|
|
430
|
+
isClientRecord: (record: any) => boolean;
|
|
442
431
|
nav: NavigationProxy<K & string, Resolved>;
|
|
443
432
|
RelationSelection: NavigationToSelection<NavigationProxy<K & string, Resolved>>;
|
|
444
433
|
createView: <const TSelection extends NavigationToSelection<NavigationProxy<K & string, Resolved>>>(selection: TSelection) => DeriveViewResult<K & string, TSelection, Resolved>;
|
|
@@ -476,7 +465,7 @@ type GetDbKey<K, Field> = Field extends Reference<infer TGetter> ? ReturnType<TG
|
|
|
476
465
|
};
|
|
477
466
|
} ? string extends F ? K : F : K;
|
|
478
467
|
type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientSchema" | "zodValidationSchema", Depth extends any[] = []> = Depth["length"] extends 10 ? any : {
|
|
479
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "
|
|
468
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? Key extends "zodSqlSchema" ? GetDbKey<K, T[K]> : K : T[K] extends {
|
|
480
469
|
config: {
|
|
481
470
|
sql: {
|
|
482
471
|
type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
|
|
@@ -493,7 +482,7 @@ type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientSchema" | "zodV
|
|
|
493
482
|
} ? ZodSchema : never;
|
|
494
483
|
};
|
|
495
484
|
type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
|
|
496
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "
|
|
485
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
|
|
497
486
|
config: {
|
|
498
487
|
sql: {
|
|
499
488
|
type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
|
|
@@ -513,7 +502,7 @@ type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] exte
|
|
|
513
502
|
} ? TNew extends TSql ? z.infer<TClient> : D extends () => infer R ? R : D : never;
|
|
514
503
|
}>;
|
|
515
504
|
type DeriveStateType<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
|
|
516
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "
|
|
505
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
|
|
517
506
|
config: {
|
|
518
507
|
sql: {
|
|
519
508
|
type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
|
package/dist/schema.js
CHANGED
|
@@ -103,7 +103,6 @@ export const s = {
|
|
|
103
103
|
});
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
|
-
// PASTE THIS ENTIRE FUNCTION OVER YOUR EXISTING createBuilder
|
|
107
106
|
function createBuilder(config) {
|
|
108
107
|
const completedStages = config.completedStages || new Set([config.stage]);
|
|
109
108
|
const builderObject = {
|
|
@@ -115,8 +114,8 @@ function createBuilder(config) {
|
|
|
115
114
|
inferDefaultFromZod(config.clientZod, config.sqlConfig),
|
|
116
115
|
zodClientSchema: config.clientZod,
|
|
117
116
|
zodValidationSchema: config.validationZod,
|
|
118
|
-
clientTransform: config.clientTransform,
|
|
119
|
-
validationTransform: config.validationTransform,
|
|
117
|
+
clientTransform: config.clientTransform,
|
|
118
|
+
validationTransform: config.validationTransform,
|
|
120
119
|
},
|
|
121
120
|
initialState: (options) => {
|
|
122
121
|
if (completedStages.has("new")) {
|
|
@@ -169,11 +168,10 @@ function createBuilder(config) {
|
|
|
169
168
|
const newCompletedStages = new Set(completedStages);
|
|
170
169
|
newCompletedStages.add("new");
|
|
171
170
|
const newConfig = { ...config.sqlConfig };
|
|
172
|
-
if (clientPk) {
|
|
173
|
-
|
|
171
|
+
if (clientPk !== undefined) {
|
|
172
|
+
// Store the boolean OR the function directly into the config
|
|
173
|
+
newConfig.isClientPk = clientPk;
|
|
174
174
|
}
|
|
175
|
-
// When clientPk is true, ALWAYS union the SQL type with the client type
|
|
176
|
-
// because records can be either DB-sourced (number) or client-created (string)
|
|
177
175
|
let clientAndServerSchema;
|
|
178
176
|
if (clientPk) {
|
|
179
177
|
// Always union for clientPk fields
|
|
@@ -221,11 +219,8 @@ function createBuilder(config) {
|
|
|
221
219
|
...config,
|
|
222
220
|
stage: "client",
|
|
223
221
|
completedStages: newCompletedStages,
|
|
224
|
-
// Store the transform function to be used later
|
|
225
222
|
clientTransform: (baseSchema) => {
|
|
226
223
|
if (isFunction(assert)) {
|
|
227
|
-
// We use `as any` here to resolve the complex generic type error.
|
|
228
|
-
// It's safe because we know the baseSchema will have the necessary Zod methods.
|
|
229
224
|
return assert({
|
|
230
225
|
sql: baseSchema,
|
|
231
226
|
initialState: config.newZod,
|
|
@@ -235,7 +230,6 @@ function createBuilder(config) {
|
|
|
235
230
|
},
|
|
236
231
|
});
|
|
237
232
|
}
|
|
238
|
-
// This is the original logic for non-relation fields
|
|
239
233
|
const clientSchema = isFunction(assert)
|
|
240
234
|
? assert({ sql: config.sqlZod, initialState: config.newZod })
|
|
241
235
|
: assert;
|
|
@@ -247,9 +241,7 @@ function createBuilder(config) {
|
|
|
247
241
|
completedStages: newCompletedStages,
|
|
248
242
|
});
|
|
249
243
|
},
|
|
250
|
-
server: (
|
|
251
|
-
// ... this validation function remains unchanged ...
|
|
252
|
-
assert) => {
|
|
244
|
+
server: (assert) => {
|
|
253
245
|
if (completedStages.has("server")) {
|
|
254
246
|
throw new Error("validation() can only be called once in the chain");
|
|
255
247
|
}
|
|
@@ -305,13 +297,9 @@ export function schema(schema) {
|
|
|
305
297
|
}
|
|
306
298
|
}
|
|
307
299
|
enrichedSchema[SchemaWrapperBrand] = true;
|
|
308
|
-
// Add private properties
|
|
309
300
|
enrichedSchema.__primaryKeySQL = undefined;
|
|
310
|
-
enrichedSchema.__isClientChecker = undefined;
|
|
311
|
-
// Add methods directly
|
|
312
301
|
enrichedSchema.primaryKeySQL = function (definer) {
|
|
313
302
|
const pkFieldsOnly = {};
|
|
314
|
-
// Find all PK fields
|
|
315
303
|
for (const key in schema) {
|
|
316
304
|
const field = schema[key];
|
|
317
305
|
if (field &&
|
|
@@ -323,29 +311,21 @@ export function schema(schema) {
|
|
|
323
311
|
enrichedSchema.__primaryKeySQL = definer(pkFieldsOnly);
|
|
324
312
|
return enrichedSchema;
|
|
325
313
|
};
|
|
326
|
-
enrichedSchema.isClient = function (checker) {
|
|
327
|
-
enrichedSchema.__isClientChecker = checker;
|
|
328
|
-
return enrichedSchema;
|
|
329
|
-
};
|
|
330
314
|
return enrichedSchema;
|
|
331
315
|
}
|
|
332
316
|
function inferDefaultFromZod(zodType, sqlConfig) {
|
|
333
|
-
// --- START OF FIX ---
|
|
334
|
-
// If the database is responsible for the default, the client shouldn't generate a value.
|
|
335
317
|
if (sqlConfig &&
|
|
336
318
|
"default" in sqlConfig &&
|
|
337
319
|
sqlConfig.default === "CURRENT_TIMESTAMP") {
|
|
338
320
|
return undefined;
|
|
339
321
|
}
|
|
340
|
-
// --- END OF FIX ---
|
|
341
322
|
if (sqlConfig && typeof sqlConfig === "object" && "type" in sqlConfig) {
|
|
342
323
|
if ("default" in sqlConfig && sqlConfig.default !== undefined) {
|
|
343
|
-
// This part now runs only if default is not CURRENT_TIMESTAMP
|
|
344
324
|
return sqlConfig.default;
|
|
345
325
|
}
|
|
346
326
|
if (typeof sqlConfig.type === "string" &&
|
|
347
327
|
["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
|
|
348
|
-
// ...
|
|
328
|
+
// ...
|
|
349
329
|
}
|
|
350
330
|
const sqlTypeConfig = sqlConfig;
|
|
351
331
|
if (sqlTypeConfig.type && !sqlTypeConfig.nullable) {
|
|
@@ -361,7 +341,7 @@ function inferDefaultFromZod(zodType, sqlConfig) {
|
|
|
361
341
|
return false;
|
|
362
342
|
case "date":
|
|
363
343
|
case "datetime":
|
|
364
|
-
case "timestamp":
|
|
344
|
+
case "timestamp":
|
|
365
345
|
return new Date();
|
|
366
346
|
}
|
|
367
347
|
}
|
|
@@ -381,7 +361,6 @@ function inferDefaultFromZod(zodType, sqlConfig) {
|
|
|
381
361
|
}
|
|
382
362
|
return undefined;
|
|
383
363
|
}
|
|
384
|
-
// Helper to check if something is a reference
|
|
385
364
|
function isReference(value) {
|
|
386
365
|
return value && typeof value === "object" && value.__type === "reference";
|
|
387
366
|
}
|
|
@@ -392,7 +371,6 @@ export function createSchema(schema, relations) {
|
|
|
392
371
|
const defaultValues = {};
|
|
393
372
|
const defaultGenerators = {};
|
|
394
373
|
const fieldTransforms = {};
|
|
395
|
-
// NEW: Track the runtime mappings
|
|
396
374
|
const clientToDbKeys = {};
|
|
397
375
|
const dbToClientKeys = {};
|
|
398
376
|
const fullSchema = { ...schema, ...(relations || {}) };
|
|
@@ -403,7 +381,6 @@ export function createSchema(schema, relations) {
|
|
|
403
381
|
if (key === "_tableName" ||
|
|
404
382
|
key.startsWith("__") ||
|
|
405
383
|
key === String(SchemaWrapperBrand) ||
|
|
406
|
-
key === "isClient" ||
|
|
407
384
|
key === "primaryKeySQL" ||
|
|
408
385
|
typeof value === "function")
|
|
409
386
|
continue;
|
|
@@ -412,11 +389,9 @@ export function createSchema(schema, relations) {
|
|
|
412
389
|
const targetField = definition.getter();
|
|
413
390
|
if (targetField && targetField.config) {
|
|
414
391
|
const config = targetField.config;
|
|
415
|
-
// Track mapping
|
|
416
392
|
const dbFieldName = config.sql?.field || key;
|
|
417
393
|
clientToDbKeys[key] = dbFieldName;
|
|
418
394
|
dbToClientKeys[dbFieldName] = key;
|
|
419
|
-
// ALL SCHEMAS STRICTLY USE THE JS KEY
|
|
420
395
|
sqlFields[dbFieldName] = config.zodSqlSchema;
|
|
421
396
|
clientFields[key] = config.zodClientSchema;
|
|
422
397
|
serverFields[key] = config.zodValidationSchema;
|
|
@@ -445,11 +420,9 @@ export function createSchema(schema, relations) {
|
|
|
445
420
|
continue;
|
|
446
421
|
}
|
|
447
422
|
else {
|
|
448
|
-
// Track mapping
|
|
449
423
|
const dbFieldName = sqlConfig?.field || key;
|
|
450
424
|
clientToDbKeys[key] = dbFieldName;
|
|
451
425
|
dbToClientKeys[dbFieldName] = key;
|
|
452
|
-
// ALL SCHEMAS STRICTLY USE THE JS KEY
|
|
453
426
|
sqlFields[dbFieldName] = config.zodSqlSchema;
|
|
454
427
|
clientFields[key] = config.zodClientSchema;
|
|
455
428
|
serverFields[key] = config.zodValidationSchema;
|
|
@@ -470,23 +443,51 @@ export function createSchema(schema, relations) {
|
|
|
470
443
|
}
|
|
471
444
|
}
|
|
472
445
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
else if (clientPkKeys.length > 0) {
|
|
479
|
-
const autoChecks = [];
|
|
446
|
+
// --- NEW: SMART CHECKER BUILDER ---
|
|
447
|
+
let isClientRecord = () => false;
|
|
448
|
+
if (clientPkKeys.length > 0) {
|
|
449
|
+
const checkers = [];
|
|
480
450
|
for (const key of clientPkKeys) {
|
|
481
451
|
const field = fullSchema[key];
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
452
|
+
const sqlConfig = field?.config?.sql;
|
|
453
|
+
const dbKey = sqlConfig?.field || key;
|
|
454
|
+
const isClientPkVal = sqlConfig?.isClientPk;
|
|
455
|
+
if (typeof isClientPkVal === "function") {
|
|
456
|
+
// Explicit checker provided directly in the field!
|
|
457
|
+
checkers.push({ clientKey: key, dbKey, check: isClientPkVal });
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Fallback auto-detection: If they just passed `true`
|
|
461
|
+
const initialValueOrFn = field?.config?.initialValue;
|
|
462
|
+
let sampleValue = initialValueOrFn;
|
|
463
|
+
// Safely execute the function once to figure out its return type!
|
|
464
|
+
if (isFunction(initialValueOrFn)) {
|
|
465
|
+
try {
|
|
466
|
+
sampleValue = initialValueOrFn({ uuid });
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
// Ignore if the factory fails with a dummy payload
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (sqlConfig?.type === "int" && typeof sampleValue === "string") {
|
|
473
|
+
checkers.push({
|
|
474
|
+
clientKey: key,
|
|
475
|
+
dbKey,
|
|
476
|
+
check: (val) => typeof val === "string",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
486
479
|
}
|
|
487
480
|
}
|
|
488
|
-
if (
|
|
489
|
-
isClientRecord = (record) =>
|
|
481
|
+
if (checkers.length > 0) {
|
|
482
|
+
isClientRecord = (record) => {
|
|
483
|
+
if (!record || typeof record !== "object")
|
|
484
|
+
return false;
|
|
485
|
+
return checkers.some(({ clientKey, dbKey, check }) => {
|
|
486
|
+
// Look at both the client shape key AND the db shape key safely
|
|
487
|
+
const val = record[clientKey] !== undefined ? record[clientKey] : record[dbKey];
|
|
488
|
+
return check(val);
|
|
489
|
+
});
|
|
490
|
+
};
|
|
490
491
|
}
|
|
491
492
|
}
|
|
492
493
|
const generateDefaults = () => {
|
|
@@ -502,7 +503,6 @@ export function createSchema(schema, relations) {
|
|
|
502
503
|
}
|
|
503
504
|
return freshDefaults;
|
|
504
505
|
};
|
|
505
|
-
// --- RUNTIME TRANSFORMERS: Map keys and apply value transforms ---
|
|
506
506
|
const toClient = (dbObject) => {
|
|
507
507
|
const clientObject = {};
|
|
508
508
|
for (const dbKey in dbObject) {
|
|
@@ -529,7 +529,6 @@ export function createSchema(schema, relations) {
|
|
|
529
529
|
}
|
|
530
530
|
return dbObject;
|
|
531
531
|
};
|
|
532
|
-
// Create the final Zod objects once
|
|
533
532
|
const finalSqlSchema = z.object(sqlFields);
|
|
534
533
|
const finalClientSchema = z.object(clientFields);
|
|
535
534
|
const finalValidationSchema = z.object(serverFields);
|
|
@@ -556,16 +555,13 @@ export function createSchema(schema, relations) {
|
|
|
556
555
|
};
|
|
557
556
|
}
|
|
558
557
|
function createViewObject(initialRegistryKey, selection, registry, tableNameToRegistryKeyMap) {
|
|
559
|
-
// Add a flag to track if all tables support reconciliation
|
|
560
558
|
let allTablesSupportsReconciliation = true;
|
|
561
|
-
// Debug: track which tables are checked
|
|
562
559
|
const checkedTables = {};
|
|
563
560
|
function buildView(currentRegistryKey, subSelection, schemaType) {
|
|
564
561
|
const registryEntry = registry[currentRegistryKey];
|
|
565
562
|
if (!registryEntry) {
|
|
566
563
|
throw new Error(`Schema with key "${currentRegistryKey}" not found in the registry.`);
|
|
567
564
|
}
|
|
568
|
-
// FIX: Check at the correct path - registryEntry.zodSchemas
|
|
569
565
|
if (!(currentRegistryKey in checkedTables)) {
|
|
570
566
|
const hasPks = !!(registryEntry.zodSchemas?.pk &&
|
|
571
567
|
registryEntry.zodSchemas.pk.length > 0 &&
|
|
@@ -599,7 +595,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
|
|
|
599
595
|
if (!nextRegistryKey) {
|
|
600
596
|
throw new Error(`Could not resolve registry key for table "${targetTableName}"`);
|
|
601
597
|
}
|
|
602
|
-
// Recursive call will also check that table's pk/clientPk
|
|
603
598
|
const relationSchema = buildView(nextRegistryKey, subSelection[relationKey], schemaType);
|
|
604
599
|
if (["hasMany", "manyToMany"].includes(relationConfig.type)) {
|
|
605
600
|
selectedRelationShapes[relationKey] = z.array(relationSchema);
|
|
@@ -621,7 +616,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
|
|
|
621
616
|
};
|
|
622
617
|
}
|
|
623
618
|
export function createSchemaBox(schemas, resolver) {
|
|
624
|
-
// Your existing implementation stays exactly the same
|
|
625
619
|
const schemaProxy = new Proxy({}, {
|
|
626
620
|
get(target, tableName) {
|
|
627
621
|
const schema = schemas[tableName];
|
|
@@ -647,14 +641,12 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
647
641
|
});
|
|
648
642
|
const resolutionConfig = resolver(schemaProxy);
|
|
649
643
|
const resolvedSchemas = schemas;
|
|
650
|
-
// STAGE 1: Resolve references
|
|
651
644
|
for (const tableName in schemas) {
|
|
652
645
|
for (const fieldName in schemas[tableName]) {
|
|
653
646
|
const field = schemas[tableName][fieldName];
|
|
654
647
|
if (isReference(field)) {
|
|
655
648
|
const targetField = field.getter();
|
|
656
649
|
if (targetField && targetField.config) {
|
|
657
|
-
// FIX: Shallow copy preserving Zod instances
|
|
658
650
|
const newConfig = {
|
|
659
651
|
...targetField.config,
|
|
660
652
|
sql: { ...targetField.config.sql, isForeignKey: true },
|
|
@@ -670,7 +662,6 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
670
662
|
}
|
|
671
663
|
}
|
|
672
664
|
}
|
|
673
|
-
// STAGE 2: Resolve relations
|
|
674
665
|
for (const tableName in schemas) {
|
|
675
666
|
const tableConfig = resolutionConfig[tableName];
|
|
676
667
|
if (!tableConfig)
|
|
@@ -744,7 +735,6 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
744
735
|
const tableName = finalRegistry[key].rawSchema._tableName;
|
|
745
736
|
tableNameToRegistryKeyMap[tableName] = key;
|
|
746
737
|
}
|
|
747
|
-
// Inside createSchemaBox, in the cleanerRegistry loop:
|
|
748
738
|
for (const tableName in finalRegistry) {
|
|
749
739
|
const entry = finalRegistry[tableName];
|
|
750
740
|
cleanerRegistry[tableName] = {
|
|
@@ -759,13 +749,11 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
759
749
|
toClient: entry.zodSchemas.toClient,
|
|
760
750
|
toDb: entry.zodSchemas.toDb,
|
|
761
751
|
},
|
|
762
|
-
// ADD: parse functions
|
|
763
752
|
parseForDb: entry.zodSchemas.parseForDb,
|
|
764
753
|
parseFromDb: entry.zodSchemas.parseFromDb,
|
|
765
754
|
defaults: entry.zodSchemas.defaultValues,
|
|
766
755
|
stateType: entry.zodSchemas.stateType,
|
|
767
756
|
generateDefaults: entry.zodSchemas.generateDefaults,
|
|
768
|
-
// ADD: PK info
|
|
769
757
|
pk: entry.zodSchemas.pk,
|
|
770
758
|
clientPk: entry.zodSchemas.clientPk,
|
|
771
759
|
isClientRecord: entry.zodSchemas.isClientRecord,
|
|
@@ -773,6 +761,32 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
773
761
|
createView: (selection) => {
|
|
774
762
|
const view = createViewObject(tableName, selection, finalRegistry, tableNameToRegistryKeyMap);
|
|
775
763
|
const defaults = computeViewDefaults(tableName, selection, finalRegistry, tableNameToRegistryKeyMap);
|
|
764
|
+
const deepToClient = (dbData, currentSelection, currentKey) => {
|
|
765
|
+
if (!dbData)
|
|
766
|
+
return dbData;
|
|
767
|
+
if (Array.isArray(dbData))
|
|
768
|
+
return dbData.map((item) => deepToClient(item, currentSelection, currentKey));
|
|
769
|
+
const regEntry = finalRegistry[currentKey];
|
|
770
|
+
const baseMapped = regEntry.zodSchemas.toClient(dbData);
|
|
771
|
+
if (typeof currentSelection === "object") {
|
|
772
|
+
for (const relKey in currentSelection) {
|
|
773
|
+
if (currentSelection[relKey] &&
|
|
774
|
+
dbData[relKey] !== undefined &&
|
|
775
|
+
dbData[relKey] !== null) {
|
|
776
|
+
const relField = regEntry.rawSchema[relKey];
|
|
777
|
+
if (relField?.config?.sql?.schema) {
|
|
778
|
+
const targetTableName = relField.config.sql.schema()._tableName;
|
|
779
|
+
const nextRegKey = tableNameToRegistryKeyMap[targetTableName];
|
|
780
|
+
if (nextRegKey) {
|
|
781
|
+
baseMapped[relKey] = deepToClient(dbData[relKey], currentSelection[relKey], nextRegKey);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return baseMapped;
|
|
788
|
+
};
|
|
789
|
+
const viewToClient = (dbData) => deepToClient(dbData, selection, tableName);
|
|
776
790
|
return {
|
|
777
791
|
definition: entry.rawSchema,
|
|
778
792
|
schemaKey: tableName,
|
|
@@ -782,10 +796,13 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
782
796
|
server: view.server,
|
|
783
797
|
},
|
|
784
798
|
transforms: {
|
|
785
|
-
toClient:
|
|
799
|
+
toClient: viewToClient,
|
|
786
800
|
toDb: entry.zodSchemas.toDb,
|
|
787
|
-
|
|
788
|
-
|
|
801
|
+
},
|
|
802
|
+
parseForDb: entry.zodSchemas.parseForDb,
|
|
803
|
+
parseFromDb: (dbData) => {
|
|
804
|
+
const mapped = viewToClient(dbData);
|
|
805
|
+
return view.client.parse(mapped);
|
|
789
806
|
},
|
|
790
807
|
defaults: defaults,
|
|
791
808
|
pk: entry.zodSchemas.pk,
|
|
@@ -798,23 +815,18 @@ export function createSchemaBox(schemas, resolver) {
|
|
|
798
815
|
};
|
|
799
816
|
},
|
|
800
817
|
RelationSelection: {},
|
|
801
|
-
__registry: finalRegistry,
|
|
818
|
+
__registry: finalRegistry,
|
|
802
819
|
};
|
|
803
820
|
}
|
|
804
821
|
return cleanerRegistry;
|
|
805
822
|
}
|
|
806
|
-
function computeViewDefaults(currentRegistryKey,
|
|
807
|
-
selection, registry, tableNameToRegistryKeyMap, // Accept the map
|
|
808
|
-
visited = new Set()) {
|
|
823
|
+
function computeViewDefaults(currentRegistryKey, selection, registry, tableNameToRegistryKeyMap, visited = new Set()) {
|
|
809
824
|
if (visited.has(currentRegistryKey)) {
|
|
810
|
-
return undefined;
|
|
825
|
+
return undefined;
|
|
811
826
|
}
|
|
812
827
|
visited.add(currentRegistryKey);
|
|
813
|
-
// This lookup now uses the correct key every time.
|
|
814
828
|
const entry = registry[currentRegistryKey];
|
|
815
|
-
// This check prevents the crash.
|
|
816
829
|
if (!entry) {
|
|
817
|
-
// This case should ideally not be hit if the map is correct, but it's safe to have.
|
|
818
830
|
console.warn(`Could not find entry for key "${currentRegistryKey}" in registry while computing defaults.`);
|
|
819
831
|
return {};
|
|
820
832
|
}
|
|
@@ -823,7 +835,6 @@ visited = new Set()) {
|
|
|
823
835
|
if (selection === true || typeof selection !== "object") {
|
|
824
836
|
return baseDefaults;
|
|
825
837
|
}
|
|
826
|
-
// Add relation defaults based on selection
|
|
827
838
|
for (const key in selection) {
|
|
828
839
|
if (!selection[key])
|
|
829
840
|
continue;
|
|
@@ -831,14 +842,10 @@ visited = new Set()) {
|
|
|
831
842
|
if (!field?.config?.sql?.schema)
|
|
832
843
|
continue;
|
|
833
844
|
const relationConfig = field.config.sql;
|
|
834
|
-
// --- THE CORE FIX ---
|
|
835
|
-
// 1. Get the internal _tableName of the related schema (e.g., "post_table")
|
|
836
845
|
const targetTableName = relationConfig.schema()._tableName;
|
|
837
|
-
// 2. Use the map to find the correct registry key for it (e.g., "posts")
|
|
838
846
|
const nextRegistryKey = tableNameToRegistryKeyMap[targetTableName];
|
|
839
847
|
if (!nextRegistryKey)
|
|
840
|
-
continue;
|
|
841
|
-
// Handle different default configurations
|
|
848
|
+
continue;
|
|
842
849
|
const defaultConfig = relationConfig.defaultConfig;
|
|
843
850
|
if (defaultConfig === undefined) {
|
|
844
851
|
delete baseDefaults[key];
|
|
@@ -852,17 +859,10 @@ visited = new Set()) {
|
|
|
852
859
|
else if (relationConfig.type === "hasMany" ||
|
|
853
860
|
relationConfig.type === "manyToMany") {
|
|
854
861
|
const count = defaultConfig?.count || relationConfig.defaultCount || 1;
|
|
855
|
-
baseDefaults[key] = Array.from({ length: count }, () =>
|
|
856
|
-
// 3. Make the recursive call with the CORRECT key
|
|
857
|
-
computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, // Pass the map along
|
|
858
|
-
new Set(visited)));
|
|
862
|
+
baseDefaults[key] = Array.from({ length: count }, () => computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, new Set(visited)));
|
|
859
863
|
}
|
|
860
864
|
else {
|
|
861
|
-
|
|
862
|
-
baseDefaults[key] =
|
|
863
|
-
// 3. Make the recursive call with the CORRECT key
|
|
864
|
-
computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, // Pass the map along
|
|
865
|
-
new Set(visited));
|
|
865
|
+
baseDefaults[key] = computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, new Set(visited));
|
|
866
866
|
}
|
|
867
867
|
}
|
|
868
868
|
return baseDefaults;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogsbox-shape",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.175",
|
|
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",
|