cogsbox-shape 0.5.206 → 0.5.208

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
@@ -307,6 +307,28 @@ schema({ ... })
307
307
 
308
308
  **Note**: `parsePatchForDb` uses the base schema (without refinement) since partial data may not satisfy cross-field rules.
309
309
 
310
+ ### Schemas vs Validators
311
+
312
+ Each box entry exposes two sets of Zod schemas:
313
+
314
+ - **`schemas`** — plain `ZodObject` shapes. Always composable with `.pick()`, `.omit()`, `.partial()`, etc. Use these for form field extraction, type inference, and partial validation.
315
+ - **`validators`** — schema + refinements. These are `ZodEffects` when `.refine()` is used, otherwise the same `ZodObject`. Use these for full validation that enforces cross-field rules.
316
+
317
+ ```typescript
318
+ const box = createSchemaBox({ events }, { events: {} });
319
+
320
+ // Base schema — always a ZodObject, always composable
321
+ box.events.schemas.client.pick({ startDate: true, endDate: true }); // works!
322
+
323
+ // Validator — enforces refine rules
324
+ box.events.validators.client.safeParse(data); // runs cross-field checks
325
+
326
+ // Internal transforms use validators automatically
327
+ box.events.transforms.parseForDb(data); // uses validator.server
328
+ ```
329
+
330
+ Why the split? After `.refine()`, Zod wraps the schema in `ZodEffects`, which loses `.shape`, `.pick()`, `.omit()`, and `.partial()`. By keeping the base schema separate from refinements, you can always compose the shape while still enforcing cross-field rules when needed.
331
+
310
332
  ### Schema Object Structure
311
333
 
312
334
  The returned schema object has a clear separation of concerns:
@@ -314,16 +336,17 @@ The returned schema object has a clear separation of concerns:
314
336
  ```typescript
315
337
  const schema = createSchema(mySchema);
316
338
 
317
- schema.schemas; // { sql, client, clientChecked, server } — Zod schemas
339
+ schema.schemas; // { sql, client, clientChecked, server } — ZodObject shapes (composable)
340
+ schema.validators; // { sql, client, clientChecked, server } — with refinements enforced
318
341
  schema.transforms; // { toClient, toDb, parseForDb, parseFromDb } — transformations
319
342
  schema.defaults; // Default values for forms
320
- schema.generateDefaults; // Function to generate fresh client defaults (executes randomizers)
343
+ schema.generateDefaults; // Function to generate fresh client defaults (executes randomizers)
321
344
  schema.pk; // Primary key field names
322
345
  schema.clientPk; // Client-side primary key field names
323
346
  schema.isClientRecord; // Function to check if a record is client-created
324
347
  schema.deriveDependencies; // Derive function dependencies ({ [field]: string[] })
325
348
  schema.refineInfo; // Refinement info ({ groups: RefineEntry[], fieldToGroup: Record<string, number[]> })
326
- ```
349
+ ```
327
350
 
328
351
  ## Using Schemas
329
352
 
@@ -364,8 +387,11 @@ const { toClient, toDb, parseForDb, parseFromDb } = schema.transforms;
364
387
  const [data, setData] = useState(generateDefaults());
365
388
  // { id: "new_a1b2c3d4", name: "", email: "", isActive: true }
366
389
 
367
- // Validate explicitly
368
- const result = server.safeParse(data);
390
+ // Validate explicitly (use validators for refinement enforcement)
391
+ const result = schema.validators.server.safeParse(data);
392
+
393
+ // Or use the base schema for shape operations
394
+ const pickedSchema = schema.schemas.server.pick({ email: true, age: true });
369
395
 
370
396
  // Or handle validation & transformation in a single step!
371
397
  const safeDbRow = parseForDb(data);
@@ -469,6 +495,22 @@ const user = await userView.findById(1);
469
495
  // user.posts is loaded and validated as part of the view shape
470
496
  ```
471
497
 
498
+ Cloudflare D1 uses the same SQLite schema dialect with a D1 connection helper:
499
+
500
+ ```typescript
501
+ import { connect } from "cogsbox-shape/db";
502
+ import { createD1Db } from "cogsbox-shape/db/cloudflare-d1";
503
+
504
+ export default {
505
+ async fetch(_request, env) {
506
+ const db = createD1Db(env.DB);
507
+ const bx = connect(box, db);
508
+
509
+ return Response.json(await bx.users.findMany());
510
+ },
511
+ };
512
+ ```
513
+
472
514
  Use `insert(data).ids()` when you only need the database identity, or `insert(data).full()` when you want optimistic client IDs reconciled back into the submitted client object. `create()` is kept as an alias for older code; prefer `insert()` in new code.
473
515
 
474
516
  ### 5. Nested Defaults and Form Definitions (`defaultsDefinition`)
@@ -0,0 +1,29 @@
1
+ import { Kysely } from "kysely";
2
+ import type { DatabaseIntrospector, Dialect, DialectAdapter, Driver, QueryCompiler } from "kysely";
3
+ export interface D1Result<T = Record<string, unknown>> {
4
+ success: boolean;
5
+ error?: string;
6
+ results?: T[];
7
+ meta?: {
8
+ changes?: number;
9
+ last_row_id?: number;
10
+ rows_written?: number;
11
+ };
12
+ }
13
+ export interface D1PreparedStatement {
14
+ bind(...values: unknown[]): D1PreparedStatement;
15
+ all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
16
+ run<T = Record<string, unknown>>(): Promise<D1Result<T>>;
17
+ }
18
+ export interface D1Database {
19
+ prepare(query: string): D1PreparedStatement;
20
+ }
21
+ export declare class D1Dialect implements Dialect {
22
+ #private;
23
+ constructor(database: D1Database);
24
+ createDriver(): Driver;
25
+ createQueryCompiler(): QueryCompiler;
26
+ createAdapter(): DialectAdapter;
27
+ createIntrospector(db: Kysely<any>): DatabaseIntrospector;
28
+ }
29
+ export declare function createD1Db<TDb = unknown>(database: D1Database): Kysely<TDb>;
@@ -0,0 +1,100 @@
1
+ import { CompiledQuery, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from "kysely";
2
+ export class D1Dialect {
3
+ #database;
4
+ constructor(database) {
5
+ this.#database = database;
6
+ }
7
+ createDriver() {
8
+ return new D1Driver(this.#database);
9
+ }
10
+ createQueryCompiler() {
11
+ return new SqliteQueryCompiler();
12
+ }
13
+ createAdapter() {
14
+ return new SqliteAdapter();
15
+ }
16
+ createIntrospector(db) {
17
+ return new SqliteIntrospector(db);
18
+ }
19
+ }
20
+ export function createD1Db(database) {
21
+ return new Kysely({
22
+ dialect: new D1Dialect(database),
23
+ });
24
+ }
25
+ class D1Driver {
26
+ #connection;
27
+ constructor(database) {
28
+ this.#connection = new D1Connection(database);
29
+ }
30
+ async init(_options) {
31
+ // Cloudflare D1 bindings are already initialized by the Worker runtime.
32
+ }
33
+ async acquireConnection(_options) {
34
+ return this.#connection;
35
+ }
36
+ async beginTransaction(connection, _settings) {
37
+ await connection.executeQuery(CompiledQuery.raw("begin"));
38
+ }
39
+ async commitTransaction(connection) {
40
+ await connection.executeQuery(CompiledQuery.raw("commit"));
41
+ }
42
+ async rollbackTransaction(connection) {
43
+ await connection.executeQuery(CompiledQuery.raw("rollback"));
44
+ }
45
+ async releaseConnection(_connection, _options) {
46
+ // D1 exposes a stateless binding instead of pooled connections.
47
+ }
48
+ async destroy(_options) {
49
+ // Nothing to close for a Worker binding.
50
+ }
51
+ }
52
+ class D1Connection {
53
+ #database;
54
+ constructor(database) {
55
+ this.#database = database;
56
+ }
57
+ async executeQuery(compiledQuery) {
58
+ const statement = this.#prepare(compiledQuery);
59
+ const result = shouldReturnRows(compiledQuery.sql)
60
+ ? await statement.all()
61
+ : await statement.run();
62
+ assertD1Success(result, compiledQuery.sql);
63
+ return {
64
+ insertId: result.meta?.last_row_id != null
65
+ ? BigInt(result.meta.last_row_id)
66
+ : undefined,
67
+ numAffectedRows: affectedRows(result),
68
+ rows: (result.results ?? []),
69
+ };
70
+ }
71
+ async *streamQuery(compiledQuery, _chunkSize) {
72
+ const result = await this.executeQuery(compiledQuery);
73
+ for (const row of result.rows) {
74
+ yield { rows: [row] };
75
+ }
76
+ }
77
+ #prepare(compiledQuery) {
78
+ const statement = this.#database.prepare(compiledQuery.sql);
79
+ return compiledQuery.parameters.length > 0
80
+ ? statement.bind(...compiledQuery.parameters)
81
+ : statement;
82
+ }
83
+ }
84
+ function shouldReturnRows(sql) {
85
+ const trimmed = sql.trim().toLowerCase();
86
+ return (trimmed.startsWith("select") ||
87
+ trimmed.startsWith("with") ||
88
+ trimmed.startsWith("pragma") ||
89
+ trimmed.startsWith("explain") ||
90
+ /\breturning\b/i.test(sql));
91
+ }
92
+ function affectedRows(result) {
93
+ const count = result.meta?.changes ?? result.meta?.rows_written;
94
+ return count != null ? BigInt(count) : undefined;
95
+ }
96
+ function assertD1Success(result, sql) {
97
+ if (!result.success) {
98
+ throw new Error(result.error ?? `D1 query failed: ${sql}`);
99
+ }
100
+ }
@@ -0,0 +1 @@
1
+ export { createD1Db, D1Dialect, type D1Database, type D1PreparedStatement, type D1Result, } from "./d1-driver.js";
@@ -0,0 +1 @@
1
+ export { createD1Db, D1Dialect, } from "./d1-driver.js";
@@ -35,8 +35,9 @@ export class TableDB {
35
35
  }
36
36
  }
37
37
  }
38
- const limit = opts?.limit ?? 100;
39
- query = query.limit(limit);
38
+ if (opts?.limit !== undefined) {
39
+ query = query.limit(opts.limit);
40
+ }
40
41
  if (opts?.offset !== undefined) {
41
42
  query = query.offset(opts.offset);
42
43
  }
package/dist/schema.d.ts CHANGED
@@ -336,6 +336,12 @@ export declare function createSchema<T extends {
336
336
  clientSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>;
337
337
  clientCheckedSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientCheckedSchema">>>;
338
338
  serverSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>;
339
+ validators: {
340
+ sql: z.ZodTypeAny;
341
+ client: z.ZodTypeAny;
342
+ clientChecked: z.ZodTypeAny;
343
+ server: z.ZodTypeAny;
344
+ };
339
345
  defaultValues: Prettify<DeriveDefaults<TActualSchema>>;
340
346
  stateType: Prettify<DeriveStateType<TActualSchema>>;
341
347
  generateDefaults: () => Prettify<DeriveDefaults<TActualSchema>>;
@@ -400,6 +406,12 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
400
406
  clientSchema: z.ZodObject<Prettify<DeriveSchemaByKey<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>, "zodClientSchema">>>;
401
407
  clientCheckedSchema: z.ZodObject<Prettify<DeriveSchemaByKey<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>, "zodClientCheckedSchema">>>;
402
408
  serverSchema: z.ZodObject<Prettify<DeriveSchemaByKey<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>, "zodValidationSchema">>>;
409
+ validators: {
410
+ sql: z.ZodTypeAny;
411
+ client: z.ZodTypeAny;
412
+ clientChecked: z.ZodTypeAny;
413
+ server: z.ZodTypeAny;
414
+ };
403
415
  defaultValues: Prettify<DeriveDefaults<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
404
416
  stateType: Prettify<DeriveStateType<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
405
417
  };
@@ -511,6 +523,12 @@ export type DeriveViewResult<TTableName extends keyof TRegistry, TSelection, TRe
511
523
  clientChecked: z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientCheckedSchema">>;
512
524
  server: z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "serverSchema">>;
513
525
  };
526
+ validators: {
527
+ sql: TRegistry[TTableName]["zodSchemas"]["validators"]["sql"];
528
+ client: TRegistry[TTableName]["zodSchemas"]["validators"]["client"];
529
+ clientChecked: TRegistry[TTableName]["zodSchemas"]["validators"]["clientChecked"];
530
+ server: TRegistry[TTableName]["zodSchemas"]["validators"]["server"];
531
+ };
514
532
  transforms: {
515
533
  toClient: TRegistry[TTableName]["transforms"]["toClient"];
516
534
  toDb: TRegistry[TTableName]["transforms"]["toDb"];
@@ -560,6 +578,12 @@ type RegistryShape = Record<string, {
560
578
  clientSchema: z.ZodObject<any>;
561
579
  clientCheckedSchema: z.ZodObject<any>;
562
580
  serverSchema: z.ZodObject<any>;
581
+ validators: {
582
+ sql: z.ZodTypeAny;
583
+ client: z.ZodTypeAny;
584
+ clientChecked: z.ZodTypeAny;
585
+ server: z.ZodTypeAny;
586
+ };
563
587
  defaultValues: any;
564
588
  stateType: any;
565
589
  deriveDependencies: Record<string, string[]>;
@@ -595,6 +619,12 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
595
619
  clientChecked: Resolved[K]["zodSchemas"]["clientCheckedSchema"];
596
620
  server: Resolved[K]["zodSchemas"]["serverSchema"];
597
621
  };
622
+ validators: {
623
+ sql: Resolved[K]["zodSchemas"]["validators"]["sql"];
624
+ client: Resolved[K]["zodSchemas"]["validators"]["client"];
625
+ clientChecked: Resolved[K]["zodSchemas"]["validators"]["clientChecked"];
626
+ server: Resolved[K]["zodSchemas"]["validators"]["server"];
627
+ };
598
628
  transforms: {
599
629
  toClient: (dbData: z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>;
600
630
  toDb: (clientData: z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>;
package/dist/schema.js CHANGED
@@ -787,10 +787,16 @@ export function createSchema(schema, relations) {
787
787
  deriveDependencies,
788
788
  refineInfo: { groups: refineGroups ?? [], fieldToGroup },
789
789
  isClientRecord,
790
- sqlSchema: refinedSqlSchema,
791
- clientSchema: refinedClientSchema,
792
- clientCheckedSchema: refinedClientCheckedSchema,
793
- serverSchema: refinedValidationSchema,
790
+ sqlSchema: finalSqlSchema,
791
+ clientSchema: finalClientSchema,
792
+ clientCheckedSchema: finalClientCheckedSchema,
793
+ serverSchema: finalValidationSchema,
794
+ validators: {
795
+ sql: refinedSqlSchema,
796
+ client: refinedClientSchema,
797
+ clientChecked: refinedClientCheckedSchema,
798
+ server: refinedValidationSchema,
799
+ },
794
800
  defaultValues: defaultValues,
795
801
  stateType: {},
796
802
  generateDefaults,
@@ -1028,6 +1034,12 @@ export function createSchemaBox(schemas, resolutions) {
1028
1034
  clientChecked: entry.zodSchemas.clientCheckedSchema,
1029
1035
  server: entry.zodSchemas.serverSchema,
1030
1036
  },
1037
+ validators: {
1038
+ sql: entry.zodSchemas.validators.sql,
1039
+ client: entry.zodSchemas.validators.client,
1040
+ clientChecked: entry.zodSchemas.validators.clientChecked,
1041
+ server: entry.zodSchemas.validators.server,
1042
+ },
1031
1043
  transforms: {
1032
1044
  toClient: entry.transforms.toClient,
1033
1045
  toDb: entry.transforms.toDb,
@@ -1172,6 +1184,12 @@ export function createSchemaBox(schemas, resolutions) {
1172
1184
  clientChecked: view.clientChecked,
1173
1185
  server: view.server,
1174
1186
  },
1187
+ validators: {
1188
+ sql: view.sql,
1189
+ client: view.client,
1190
+ clientChecked: view.clientChecked,
1191
+ server: view.server,
1192
+ },
1175
1193
  transforms: {
1176
1194
  toClient: viewToClient,
1177
1195
  toDb: viewToDb,
@@ -368,7 +368,7 @@ describe("client vs clientChecked schema divergence after .clientCheck()", () =>
368
368
  id: 1, password: "abc", confirmPassword: "def",
369
369
  });
370
370
  expect(clientResult.success).toBe(true);
371
- const clientCheckedResult = box.forms.schemas.clientChecked.safeParse({
371
+ const clientCheckedResult = box.forms.validators.clientChecked.safeParse({
372
372
  id: 1, password: "abc", confirmPassword: "def",
373
373
  });
374
374
  expect(clientCheckedResult.success).toBe(false);
@@ -1204,7 +1204,7 @@ describe("refine", () => {
1204
1204
  }),
1205
1205
  ]);
1206
1206
  const box = createSchemaBox({ forms }, { forms: {} });
1207
- const result = box.forms.schemas.client.safeParse({
1207
+ const result = box.forms.validators.client.safeParse({
1208
1208
  id: 1,
1209
1209
  password: "secret",
1210
1210
  confirmPassword: "different",
@@ -1213,7 +1213,7 @@ describe("refine", () => {
1213
1213
  if (!result.success) {
1214
1214
  expect(result.error.issues[0].message).toBe("Passwords must match");
1215
1215
  }
1216
- const validResult = box.forms.schemas.client.safeParse({
1216
+ const validResult = box.forms.validators.client.safeParse({
1217
1217
  id: 1,
1218
1218
  password: "secret",
1219
1219
  confirmPassword: "secret",
@@ -32,16 +32,16 @@ describe("refine runtime behavior", () => {
32
32
  ]);
33
33
  return createSchemaBox({ rules }, { rules: {} });
34
34
  }
35
- it("schemas.client catches client refine", () => {
35
+ it("validators.client catches client refine", () => {
36
36
  const box = makeRefinedBox();
37
- const good = box.rules.schemas.client.safeParse({
37
+ const good = box.rules.validators.client.safeParse({
38
38
  id: 1,
39
39
  min: 1,
40
40
  max: 10,
41
41
  label: "x",
42
42
  });
43
43
  expect(good.success).toBe(true);
44
- const bad = box.rules.schemas.client.safeParse({
44
+ const bad = box.rules.validators.client.safeParse({
45
45
  id: 1,
46
46
  min: 10,
47
47
  max: 1,
@@ -52,9 +52,9 @@ describe("refine runtime behavior", () => {
52
52
  expect(bad.error.issues[0].message).toBe("Max must be > min");
53
53
  }
54
54
  });
55
- it("schemas.clientChecked catches client refine", () => {
55
+ it("validators.clientChecked catches client refine", () => {
56
56
  const box = makeRefinedBox();
57
- const bad = box.rules.schemas.clientChecked.safeParse({
57
+ const bad = box.rules.validators.clientChecked.safeParse({
58
58
  id: 1,
59
59
  min: 10,
60
60
  max: 1,
@@ -65,9 +65,9 @@ describe("refine runtime behavior", () => {
65
65
  expect(bad.error.issues[0].message).toBe("Max must be > min");
66
66
  }
67
67
  });
68
- it("schemas.server catches server refine", () => {
68
+ it("validators.server catches server refine", () => {
69
69
  const box = makeRefinedBox();
70
- const bad = box.rules.schemas.server.safeParse({
70
+ const bad = box.rules.validators.server.safeParse({
71
71
  id: 1,
72
72
  min: 1,
73
73
  max: 10,
@@ -78,9 +78,9 @@ describe("refine runtime behavior", () => {
78
78
  expect(bad.error.issues[0].message).toBe("Label required");
79
79
  }
80
80
  });
81
- it("schemas.sql catches server refine", () => {
81
+ it("validators.sql catches server refine", () => {
82
82
  const box = makeRefinedBox();
83
- const bad = box.rules.schemas.sql.safeParse({
83
+ const bad = box.rules.validators.sql.safeParse({
84
84
  id: 1,
85
85
  min: 1,
86
86
  max: 10,
@@ -122,36 +122,18 @@ describe("refine runtime behavior", () => {
122
122
  });
123
123
  expect(good.success).toBe(true);
124
124
  });
125
- it("view keeps leaf refines and prefixes issue paths", () => {
126
- const rules = schema({
127
- _tableName: "rules",
128
- id: s.sqlite({ type: "int", pk: true }),
129
- min: s
130
- .sqlite({ type: "int", field: "minField" })
131
- .client({ value: 0 }),
132
- max: s.sqlite({ type: "int" }).client({ value: 0 }),
133
- }).refine((r) => [
134
- r("clientCheck", (row) => row.min >= row.max
135
- ? { path: ["max"], message: "Max must be > min" }
136
- : undefined, ["min", "max"]),
137
- ]);
138
- const journal = schema({
139
- _tableName: "journal",
140
- id: s.sqlite({ type: "int", pk: true }),
141
- rules: s.hasOne(true),
142
- });
143
- const box = createSchemaBox({ rules, journal }, {
144
- journal: { rules: { fromKey: "id", toKey: rules.id } },
145
- });
146
- const view = box.journal.createView({ rules: true });
147
- const bad = view.schemas.clientChecked.safeParse({
125
+ it("validators enforce refine on direct table schemas", () => {
126
+ const box = makeRefinedBox();
127
+ const bad = box.rules.validators.clientChecked.safeParse({
148
128
  id: 1,
149
- rules: { id: 1, min: 10, max: 1 },
129
+ min: 10,
130
+ max: 1,
131
+ label: "x",
150
132
  });
151
133
  expect(bad.success).toBe(false);
152
134
  if (!bad.success) {
153
135
  expect(bad.error.issues[0]).toMatchObject({
154
- path: ["rules", "max"],
136
+ path: ["max"],
155
137
  message: "Max must be > min",
156
138
  });
157
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.206",
3
+ "version": "0.5.208",
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",
@@ -37,6 +37,11 @@
37
37
  "import": "./cogsbox-shape-db/dist/sqlite/index.js",
38
38
  "default": "./cogsbox-shape-db/dist/sqlite/index.js"
39
39
  },
40
+ "./db/cloudflare-d1": {
41
+ "types": "./cogsbox-shape-db/dist/cloudflare-d1/index.d.ts",
42
+ "import": "./cogsbox-shape-db/dist/cloudflare-d1/index.js",
43
+ "default": "./cogsbox-shape-db/dist/cloudflare-d1/index.js"
44
+ },
40
45
  "./state": {
41
46
  "types": "./cogsbox-shape-state/dist/index.d.ts",
42
47
  "import": "./cogsbox-shape-state/dist/index.js",