cogsbox-shape 0.5.193 → 0.5.194

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
@@ -44,7 +44,7 @@ s.sqlite()/s.postgres()/s.mysql() → .clientInput() → .client() → .se
44
44
  | `.server(fn)` | Server-side validation. Stricter rules before database writes. |
45
45
  | `.transform({ toClient, toDb })` | Converts between database and client representations. |
46
46
 
47
- Note: `.derive()` is a schema-level method, not chainable on individual fields.
47
+ Note: `.derive()` and `.refine()` are schema-level methods, not chainable on individual fields.
48
48
 
49
49
  ### 1. SQL — Define Your Database Schema
50
50
 
@@ -231,9 +231,71 @@ const users = schema({
231
231
  });
232
232
  ```
233
233
 
234
- During partial ORM updates, DB-backed derivations fetch only missing dependency fields they actually read, then recompute the affected `forDb` fields. Client-only derived fields are ignored by SQL writes.
234
+ During partial ORM updates, DB-backed derivations fetch only missing dependency fields they actually read, then recompute the affected `forDb` fields. Client-only derived fields are ignored by SQL writes.
235
235
 
236
- ### Schema Object Structure
236
+ ### 7. Refinement (`.refine()`)
237
+
238
+ `.refine()` adds cross-field validation rules that the entire row must satisfy. Unlike `.client()`/`.server()` which validate individual fields, `refine` can check relationships between fields.
239
+
240
+ ```typescript
241
+ const events = schema({
242
+ _tableName: "events",
243
+ id: s.sqlite({ type: "int", pk: true }),
244
+ startDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
245
+ endDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
246
+ content: s.sqlite({ type: "varchar", nullable: true }).clientInput({
247
+ value: null,
248
+ schema: z.string().nullable(),
249
+ }),
250
+ isPublished: s.sqlite({ type: "boolean" }).clientInput({ value: false }),
251
+ }).refine({
252
+ server: (row) => {
253
+ const errors: { path: string[]; message: string }[] = [];
254
+ if (row.startDate && row.endDate && row.startDate > row.endDate) {
255
+ errors.push({ path: ["endDate"], message: "End date must be after start date" });
256
+ }
257
+ if (row.isPublished && !row.content) {
258
+ errors.push({ path: ["content"], message: "Published events must have content" });
259
+ }
260
+ return errors.length > 0 ? errors : undefined;
261
+ },
262
+ });
263
+
264
+ const box = createSchemaBox({ events }, { events: {} });
265
+
266
+ // Server refinement runs on parseForDb (before DB writes)
267
+ box.events.transforms.parseForDb({
268
+ id: 1, startDate: "2024-12-31", endDate: "2024-01-01",
269
+ content: null, isPublished: false,
270
+ });
271
+ // Throws: "End date must be after start date"
272
+ ```
273
+
274
+ The `refine()` config accepts two optional callbacks:
275
+
276
+ | Callback | Runs on | Purpose |
277
+ |----------|---------|---------|
278
+ | `server` | `parseForDb()` | Cross-field validation before DB writes |
279
+ | `client` | `clientInput` schema | Cross-field validation on client input |
280
+
281
+ Each callback receives the full row and returns:
282
+ - `undefined` or `null` — validation passes
283
+ - A single `{ path: string[]; message: string }` — one error
284
+ - An array of `{ path: string[]; message: string }` — multiple errors
285
+
286
+ **Dependency tracking**: Same proxy-based approach as `derive()` — the library tracks which fields the refine function reads. This is used by the ORM to know which fields to include during partial updates. The same caveat applies: conditional branches with falsy defaults can hide dependencies.
287
+
288
+ **Chaining**: `refine()` can be chained after `derive()`:
289
+
290
+ ```typescript
291
+ schema({ ... })
292
+ .derive({ forDb: { fullName: (row) => `${row.firstName} ${row.lastName}` } })
293
+ .refine({ server: (row) => { ... } });
294
+ ```
295
+
296
+ **Note**: `parsePatchForDb` uses the base schema (without refinement) since partial data may not satisfy cross-field rules.
297
+
298
+ ### Schema Object Structure
237
299
 
238
300
  The returned schema object has a clear separation of concerns:
239
301
 
@@ -247,6 +309,8 @@ schema.generateDefaults; // Function to generate fresh defaults (executes random
247
309
  schema.pk; // Primary key field names
248
310
  schema.clientPk; // Client-side primary key field names
249
311
  schema.isClientRecord; // Function to check if a record is client-created
312
+ schema.deriveDependencies; // Derive function dependencies ({ [field]: string[] })
313
+ schema.refineDependencies; // Refinement dependencies ({ server: string[], client: string[] })
250
314
  ```
251
315
 
252
316
  ## Using Schemas
@@ -6,7 +6,7 @@ type Row<T> = T extends readonly (infer TItem)[] ? TItem : T;
6
6
  type Prettify<T> = {
7
7
  [K in keyof T]: T[K];
8
8
  } & {};
9
- type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "primaryKeySQL" | "derive";
9
+ type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "__refinements" | "primaryKeySQL" | "derive" | "refine";
10
10
  type SqlConfigOf<TField> = TField extends {
11
11
  config: {
12
12
  sql: infer TSql;
@@ -54,9 +54,7 @@ type IsDerivedDbField<TTable, TKey> = TTable extends {
54
54
  forDb?: infer TForDb;
55
55
  };
56
56
  };
57
- } ? TKey extends keyof NonNullable<TForDb> ? true : false : TTable extends {
58
- deriveDependencies: infer TDerives;
59
- } ? TKey extends keyof TDerives ? true : false : false;
57
+ } ? TKey extends keyof NonNullable<TForDb> ? true : false : false;
60
58
  type SqlOnlyInput<T> = T extends {
61
59
  definition: infer TDefinition;
62
60
  } ? Prettify<{
package/dist/schema.d.ts CHANGED
@@ -205,6 +205,11 @@ export type EnrichFields<T extends ShapeSchema> = {
205
205
  [K in keyof T]: K extends "_tableName" ? T[K] : K extends string ? EnrichedField<K, T[K], T> : T[K];
206
206
  };
207
207
  export declare const SchemaWrapperBrand: unique symbol;
208
+ export type RefinementError = {
209
+ path: string[];
210
+ message: string;
211
+ };
212
+ export type RefinementFn<T> = (row: T) => RefinementError | RefinementError[] | undefined | null;
208
213
  type PickPrimaryKeys<T extends ShapeSchema> = {
209
214
  [K in keyof T as T[K] extends {
210
215
  config: {
@@ -237,6 +242,10 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
237
242
  forClient?: Record<string, (row: any) => any>;
238
243
  forDb?: Record<string, (row: any) => any>;
239
244
  };
245
+ __refinements?: {
246
+ server?: RefinementFn<InferClientRow<T>>;
247
+ client?: RefinementFn<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<EnrichFields<T>, "zodClientInputSchema">>>>>;
248
+ };
240
249
  primaryKeySQL: (definer: (pkFields: PickPrimaryKeys<T>) => string) => SchemaBuilder<T>;
241
250
  derive: (derivers: {
242
251
  forClient?: {
@@ -246,6 +255,10 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
246
255
  [K in PickDbFieldKeys<T>]?: (row: InferClientRow<T>) => any;
247
256
  };
248
257
  }) => SchemaBuilder<T>;
258
+ refine: (refinements: {
259
+ server?: RefinementFn<InferClientRow<T>>;
260
+ client?: RefinementFn<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<EnrichFields<T>, "zodClientInputSchema">>>>>;
261
+ }) => SchemaBuilder<T>;
249
262
  };
250
263
  export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
251
264
  export type RelationType = "hasMany" | "hasOne" | "manyToMany";
@@ -285,6 +298,10 @@ export declare function createSchema<T extends {
285
298
  pk: string[] | null;
286
299
  clientPk: string[] | null;
287
300
  deriveDependencies: Record<string, string[]>;
301
+ refineDependencies: {
302
+ server: string[];
303
+ client: string[];
304
+ };
288
305
  isClientRecord: (record: any) => boolean;
289
306
  sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
290
307
  clientInputSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientInputSchema">>>;
@@ -366,6 +383,10 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
366
383
  };
367
384
  pk: string[] | null;
368
385
  clientPk: string[] | null;
386
+ refineDependencies: {
387
+ server: string[];
388
+ client: string[];
389
+ };
369
390
  isClientRecord: (record: any) => boolean;
370
391
  generateDefaults: () => Prettify<DeriveDefaults<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
371
392
  };
@@ -512,6 +533,10 @@ type RegistryShape = Record<string, {
512
533
  defaultValues: any;
513
534
  stateType: any;
514
535
  deriveDependencies: Record<string, string[]>;
536
+ refineDependencies: {
537
+ server: string[];
538
+ client: string[];
539
+ };
515
540
  };
516
541
  transforms: {
517
542
  toClient: (dbObject: any) => any;
@@ -523,6 +548,10 @@ type RegistryShape = Record<string, {
523
548
  pk: string[] | null;
524
549
  clientPk: string[] | null;
525
550
  deriveDependencies: Record<string, string[]>;
551
+ refineDependencies: {
552
+ server: string[];
553
+ client: string[];
554
+ };
526
555
  isClientRecord: (record: any) => boolean;
527
556
  generateDefaults: () => any;
528
557
  }>;
@@ -549,6 +578,11 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
549
578
  generateDefaults: () => Resolved[K]["zodSchemas"]["defaultValues"];
550
579
  pk: string[] | null;
551
580
  clientPk: string[] | null;
581
+ deriveDependencies: Record<string, string[]>;
582
+ refineDependencies: {
583
+ server: string[];
584
+ client: string[];
585
+ };
552
586
  isClientRecord: (record: any) => boolean;
553
587
  nav: NavigationProxy<K & string, Resolved>;
554
588
  RelationSelection: NavigationToSelection<NavigationProxy<K & string, Resolved>>;
@@ -574,7 +608,7 @@ type GetDbKey<K, Field> = Field extends Reference<infer TGetter> ? ReturnType<TG
574
608
  };
575
609
  } ? string extends F ? K : F : K;
576
610
  type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientInputSchema" | "zodClientSchema" | "zodValidationSchema", Depth extends any[] = []> = Depth["length"] extends 10 ? any : {
577
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
611
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
578
612
  config: {
579
613
  sql: {
580
614
  sqlOnly: true;
@@ -601,7 +635,7 @@ type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientInputSchema" |
601
635
  } ? ZodSchema : never;
602
636
  };
603
637
  type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
604
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
638
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
605
639
  config: {
606
640
  sql: {
607
641
  sqlOnly: true;
@@ -624,7 +658,7 @@ type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] exte
624
658
  } ? z.infer<TClient> : never;
625
659
  }>;
626
660
  type DeriveStateType<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
627
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
661
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
628
662
  config: {
629
663
  sql: {
630
664
  sqlOnly: true;
package/dist/schema.js CHANGED
@@ -357,6 +357,7 @@ export function schema(schema) {
357
357
  enrichedSchema[SchemaWrapperBrand] = true;
358
358
  enrichedSchema.__primaryKeySQL = undefined;
359
359
  enrichedSchema.__derives = undefined;
360
+ enrichedSchema.__refinements = undefined;
360
361
  enrichedSchema.primaryKeySQL = function (definer) {
361
362
  const pkFieldsOnly = {};
362
363
  for (const key in schema) {
@@ -374,6 +375,10 @@ export function schema(schema) {
374
375
  enrichedSchema.__derives = derivers;
375
376
  return enrichedSchema;
376
377
  };
378
+ enrichedSchema.refine = function (refinements) {
379
+ enrichedSchema.__refinements = refinements;
380
+ return enrichedSchema;
381
+ };
377
382
  return enrichedSchema;
378
383
  }
379
384
  function inferDefaultFromZod(zodType, sqlConfig) {
@@ -440,6 +445,7 @@ export function createSchema(schema, relations) {
440
445
  let pkKeys = [];
441
446
  let clientPkKeys = [];
442
447
  const derives = schema.__derives;
448
+ const refinements = schema.__refinements;
443
449
  for (const key in fullSchema) {
444
450
  const value = fullSchema[key];
445
451
  if (key === "_tableName" ||
@@ -678,22 +684,78 @@ export function createSchema(schema, relations) {
678
684
  };
679
685
  trackDeriveDependencies(derives?.forClient);
680
686
  trackDeriveDependencies(derives?.forDb);
687
+ const refineDependencies = { server: [], client: [] };
688
+ const trackRefinementDependencies = (fn, key) => {
689
+ if (!fn)
690
+ return;
691
+ const accessed = new Set();
692
+ const trackingRow = new Proxy(defaultValues, {
693
+ get(target, prop, receiver) {
694
+ if (typeof prop === "string") {
695
+ accessed.add(prop);
696
+ }
697
+ return Reflect.get(target, prop, receiver);
698
+ },
699
+ });
700
+ try {
701
+ fn(trackingRow);
702
+ }
703
+ catch (e) { }
704
+ refineDependencies[key] = Array.from(accessed);
705
+ };
706
+ trackRefinementDependencies(refinements?.server, "server");
707
+ trackRefinementDependencies(refinements?.client, "client");
708
+ let refinedValidationSchema = finalValidationSchema;
709
+ if (refinements?.server) {
710
+ const serverRefine = refinements.server;
711
+ refinedValidationSchema = finalValidationSchema.superRefine((data, ctx) => {
712
+ const result = serverRefine(data);
713
+ if (!result)
714
+ return;
715
+ const errors = Array.isArray(result) ? result : [result];
716
+ for (const err of errors) {
717
+ ctx.addIssue({
718
+ code: z.ZodIssueCode.custom,
719
+ message: err.message,
720
+ path: err.path,
721
+ });
722
+ }
723
+ });
724
+ }
725
+ let refinedClientInputSchema = finalClientInputSchema;
726
+ if (refinements?.client) {
727
+ const clientRefine = refinements.client;
728
+ refinedClientInputSchema = finalClientInputSchema.superRefine((data, ctx) => {
729
+ const result = clientRefine(data);
730
+ if (!result)
731
+ return;
732
+ const errors = Array.isArray(result) ? result : [result];
733
+ for (const err of errors) {
734
+ ctx.addIssue({
735
+ code: z.ZodIssueCode.custom,
736
+ message: err.message,
737
+ path: err.path,
738
+ });
739
+ }
740
+ });
741
+ }
681
742
  return {
682
743
  pk: pkKeys.length ? pkKeys : null,
683
744
  clientPk: clientPkKeys.length ? clientPkKeys : null,
684
745
  deriveDependencies,
746
+ refineDependencies,
685
747
  isClientRecord,
686
748
  sqlSchema: finalSqlSchema,
687
- clientInputSchema: finalClientInputSchema,
749
+ clientInputSchema: refinedClientInputSchema,
688
750
  clientSchema: finalClientSchema,
689
- serverSchema: finalValidationSchema,
751
+ serverSchema: refinedValidationSchema,
690
752
  defaultValues: defaultValues,
691
753
  stateType: {},
692
754
  generateDefaults,
693
755
  toClient,
694
756
  toDb,
695
757
  parseForDb: (appData) => {
696
- const validData = finalValidationSchema.parse(appData);
758
+ const validData = refinedValidationSchema.parse(appData);
697
759
  return toDb(validData);
698
760
  },
699
761
  parsePatchForDb: (patchData) => {
@@ -849,6 +911,7 @@ export function createSchemaBox(schemas, resolutions) {
849
911
  pk: zodSchemas.pk,
850
912
  clientPk: zodSchemas.clientPk,
851
913
  deriveDependencies: zodSchemas.deriveDependencies,
914
+ refineDependencies: zodSchemas.refineDependencies,
852
915
  isClientRecord: zodSchemas.isClientRecord,
853
916
  generateDefaults: zodSchemas.generateDefaults,
854
917
  };
@@ -934,6 +997,7 @@ export function createSchemaBox(schemas, resolutions) {
934
997
  pk: entry.pk,
935
998
  clientPk: entry.clientPk,
936
999
  deriveDependencies: entry.deriveDependencies,
1000
+ refineDependencies: entry.refineDependencies,
937
1001
  isClientRecord: entry.isClientRecord,
938
1002
  nav: createNavProxy(tableName, finalRegistry),
939
1003
  createView: (selection) => {
@@ -0,0 +1 @@
1
+ export {};