cogsbox-shape 0.5.192 → 0.5.193

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
@@ -33,12 +33,12 @@ Traditional approaches require defining these layers separately, leading to type
33
33
  Define a field by chaining methods. Each step is optional — use only what you need.
34
34
 
35
35
  ```
36
- s.sql() → .clientInput() → .client() → .server() → .transform()
36
+ s.sqlite()/s.postgres()/s.mysql() → .clientInput() → .client() → .server() → .transform()
37
37
  ```
38
38
 
39
39
  | Method | Purpose |
40
40
  | --------------------------------- | -------------------------------------------------------------- |
41
- | `s.sql({ type, sqlOnly })` | Database column type. `sqlOnly` excludes from client layer. |
41
+ | `s.sqlite/postgres/mysql({ type, sqlOnly })` | Database column type. `sqlOnly` excludes from client layer. |
42
42
  | `.clientInput({ value, schema })` | Client-side input schema and default value for new records. |
43
43
  | `.client(fn)` | Client-side validation on the final client union type. |
44
44
  | `.server(fn)` | Server-side validation. Stricter rules before database writes. |
@@ -46,22 +46,47 @@ s.sql() → .clientInput() → .client() → .server() → .transform()
46
46
 
47
47
  Note: `.derive()` is a schema-level method, not chainable on individual fields.
48
48
 
49
- ### 1. SQL — Define Your Database Schema
50
-
51
- Start with your database reality:
49
+ ### 1. SQL — Define Your Database Schema
50
+
51
+ Start with your database reality:
52
52
 
53
53
  ```typescript
54
54
  import { s, schema } from "cogsbox-shape";
55
55
 
56
56
  const userSchema = schema({
57
57
  _tableName: "users",
58
- id: s.sql({ type: "int", pk: true }),
59
- email: s.sql({ type: "varchar", length: 255 }),
60
- createdAt: s.sql({ type: "datetime", default: "CURRENT_TIMESTAMP" }),
58
+ id: s.sqlite({ type: "int", pk: true }),
59
+ email: s.sqlite({ type: "varchar", length: 255 }),
60
+ createdAt: s.sqlite({ type: "datetime", default: "CURRENT_TIMESTAMP" }),
61
61
  });
62
62
  ```
63
63
 
64
- This generates a Zod schema matching your SQL types exactly.
64
+ This generates a Zod schema matching your SQL types exactly.
65
+
66
+ Use the SQL engine function that matches the database this schema targets:
67
+
68
+ ```typescript
69
+ s.sqlite({ type: "text" });
70
+ s.postgres({ type: "varchar", length: 255 });
71
+ s.mysql({ type: "varchar", length: 255 });
72
+ ```
73
+
74
+ Enums are real SQL column configs:
75
+
76
+ ```typescript
77
+ s.sqlite({ type: "enum", values: ["draft", "published"] });
78
+ // SQL: TEXT CHECK (...)
79
+
80
+ s.postgres({
81
+ type: "enum",
82
+ name: "post_status",
83
+ values: ["draft", "published"],
84
+ });
85
+ // SQL: CREATE TYPE post_status AS ENUM (...), then column uses post_status
86
+
87
+ s.mysql({ type: "enum", values: ["draft", "published"] });
88
+ // SQL: ENUM('draft', 'published')
89
+ ```
65
90
 
66
91
  ### 2. Client Input — Defaults and Client-Side Validation
67
92
 
@@ -71,7 +96,7 @@ This generates a Zod schema matching your SQL types exactly.
71
96
  const userSchema = schema({
72
97
  _tableName: "users",
73
98
  // DB stores auto-increment integers, but new records need a temp string ID
74
- id: s.sql({ type: "int", pk: true }).clientInput({
99
+ id: s.sqlite({ type: "int", pk: true }).clientInput({
75
100
  value: () => crypto.randomUUID(),
76
101
  schema: z.string(),
77
102
  }),
@@ -79,12 +104,12 @@ const userSchema = schema({
79
104
  // Default value: a generated UUID string
80
105
 
81
106
  // Simple default without type override
82
- name: s.sql({ type: "varchar" }).clientInput({ value: "Anonymous" }),
107
+ name: s.sqlite({ type: "varchar" }).clientInput({ value: "Anonymous" }),
83
108
  // clientInput type: string (inherits from SQL)
84
109
  // Default value: "Anonymous"
85
110
 
86
111
  // Type-only override (no default value change)
87
- count: s.sql({ type: "int" }).clientInput(() => z.number().min(0)),
112
+ count: s.sqlite({ type: "int" }).clientInput(() => z.number().min(0)),
88
113
  // clientInput type: number (with min validation)
89
114
  // Default value: inferred from type (0 for number)
90
115
  });
@@ -97,7 +122,7 @@ const userSchema = schema({
97
122
  `.client()` adds validation rules to the final `client` schema (the union of sql | clientInput). Use it for client-side validation that operates on the complete client type.
98
123
 
99
124
  ```typescript
100
- name: s.sql({ type: "varchar" })
125
+ name: s.sqlite({ type: "varchar" })
101
126
  .clientInput({ value: "" })
102
127
  .client((tools) => tools.clientInput.min(3, "Too short"))
103
128
  .server((tools) => tools.clientInput.min(5, "Must be at least 5 chars")),
@@ -113,11 +138,11 @@ The `.client()` callback receives `tools` with `sql`, `clientInput`, and `client
113
138
  const userSchema = schema({
114
139
  _tableName: "users",
115
140
  email: s
116
- .sql({ type: "varchar", length: 255 })
141
+ .sqlite({ type: "varchar", length: 255 })
117
142
  .server(({ sql }) => sql.email("Invalid email")),
118
143
 
119
144
  age: s
120
- .sql({ type: "int" })
145
+ .sqlite({ type: "int" })
121
146
  .server(({ sql }) => sql.min(18, "Must be 18+").max(120)),
122
147
  });
123
148
  ```
@@ -126,7 +151,7 @@ The callback receives the previous schema in the chain so you can refine it:
126
151
 
127
152
  ```typescript
128
153
  name: s
129
- .sql({ type: "varchar" })
154
+ .sqlite({ type: "varchar" })
130
155
  .clientInput(() => z.string().trim())
131
156
  .server(({ clientInput }) => clientInput.min(2, "Too short")),
132
157
  ```
@@ -137,7 +162,7 @@ name: s
137
162
 
138
163
  ```typescript
139
164
  status: s
140
- .sql({ type: "int" }) // DB: 0 or 1
165
+ .sqlite({ type: "int" }) // DB: 0 or 1
141
166
  .clientInput(() => z.enum(["active", "inactive"])) // Client input: string enum
142
167
  .transform({
143
168
  toClient: (dbValue) => dbValue === 1 ? "active" : "inactive",
@@ -158,9 +183,9 @@ Use `sqlOnly: true` to define fields that belong to the database exclusively (li
158
183
  ```typescript
159
184
  const userSchema = schema({
160
185
  _tableName: "users",
161
- id: s.sql({ type: "int", pk: true }),
162
- email: s.sql({ type: "varchar" }),
163
- internalToken: s.sql({ type: "varchar", sqlOnly: true }),
186
+ id: s.sqlite({ type: "int", pk: true }),
187
+ email: s.sqlite({ type: "varchar" }),
188
+ internalToken: s.sqlite({ type: "varchar", sqlOnly: true }),
164
189
  });
165
190
  // DB reads/writes: { id, email, internalToken }
166
191
  // Client sees: { id, email }
@@ -168,12 +193,12 @@ const userSchema = schema({
168
193
 
169
194
  #### Client-Only Fields
170
195
 
171
- By skipping `s.sql()` entirely and just using `s.clientInput()`, you can define fields that exist purely on the client (like a temporary UI state or computed field) and will not be sent to the database.
196
+ By skipping `s.sqlite()` entirely and just using `s.clientInput()`, you can define fields that exist purely on the client (like a temporary UI state or computed field) and will not be sent to the database.
172
197
 
173
198
  ```typescript
174
199
  const products = schema({
175
200
  _tableName: "products",
176
- price: s.sql({ type: "int" }),
201
+ price: s.sqlite({ type: "int" }),
177
202
  formattedPrice: s.clientInput(""), // Client-only field!
178
203
  });
179
204
  ```
@@ -188,14 +213,14 @@ const products = schema({
188
213
  ```typescript
189
214
  const users = schema({
190
215
  _tableName: "users",
191
- firstName: s.sql({ type: "varchar" }).clientInput({ value: "John" }),
192
- lastName: s.sql({ type: "varchar" }).clientInput({ value: "Doe" }),
216
+ firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
217
+ lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
193
218
 
194
219
  // Virtual field. It exists in app/view state, not SQL.
195
220
  fullName: s.clientInput(""),
196
221
 
197
222
  // Hidden DB column. It is written to SQL, but not sent to the client.
198
- searchIndex: s.sql({ type: "varchar", sqlOnly: true }),
223
+ searchIndex: s.sqlite({ type: "varchar", sqlOnly: true }),
199
224
  }).derive({
200
225
  forClient: {
201
226
  fullName: (row) => `${row.firstName} ${row.lastName}`,
@@ -235,14 +260,14 @@ import { s, schema, createSchema } from "cogsbox-shape";
235
260
 
236
261
  const contactSchema = schema({
237
262
  _tableName: "contacts",
238
- id: s.sql({ type: "int", pk: true }).clientInput({
263
+ id: s.sqlite({ type: "int", pk: true }).clientInput({
239
264
  value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
240
265
  schema: z.string(),
241
266
  }),
242
- name: s.sql({ type: "varchar" }).server(({ sql }) => sql.min(2)),
243
- email: s.sql({ type: "varchar" }).server(({ sql }) => sql.email()),
267
+ name: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.min(2)),
268
+ email: s.sqlite({ type: "varchar" }).server(({ sql }) => sql.email()),
244
269
  isActive: s
245
- .sql({ type: "boolean", default: true })
270
+ .sqlite({ type: "boolean", default: true })
246
271
  .clientInput(() => z.boolean())
247
272
  .transform({
248
273
  toClient: (val) => Boolean(val),
@@ -282,15 +307,15 @@ import { s, schema, createSchemaBox } from "cogsbox-shape";
282
307
 
283
308
  const users = schema({
284
309
  _tableName: "users",
285
- id: s.sql({ type: "int", pk: true }),
286
- name: s.sql({ type: "varchar" }),
310
+ id: s.sqlite({ type: "int", pk: true }),
311
+ name: s.sqlite({ type: "varchar" }),
287
312
  posts: s.hasMany(), // Placeholder — resolved later
288
313
  });
289
314
 
290
315
  const posts = schema({
291
316
  _tableName: "posts",
292
- id: s.sql({ type: "int", pk: true }),
293
- title: s.sql({ type: "varchar" }),
317
+ id: s.sqlite({ type: "int", pk: true }),
318
+ title: s.sqlite({ type: "varchar" }),
294
319
  authorId: s.reference(() => users.id), // Foreign key
295
320
  });
296
321
  ```
@@ -364,7 +389,7 @@ const userView = bx.users.createView({
364
389
  posts: true,
365
390
  });
366
391
 
367
- const user = await userView.db.findById(1);
392
+ const user = await userView.findById(1);
368
393
  // user.posts is loaded and validated as part of the view shape
369
394
  ```
370
395
 
@@ -1,7 +1,8 @@
1
1
  import { Kysely } from "kysely";
2
- import { TableDB } from "./table-db.js";
2
+ import type { TableDBApi } from "./table-db.js";
3
3
  type FirstArg<T> = T extends (arg: infer A, ...args: any[]) => any ? A : never;
4
4
  type Return<T> = T extends (...args: any[]) => infer R ? R : never;
5
+ type Row<T> = T extends readonly (infer TItem)[] ? TItem : T;
5
6
  type Prettify<T> = {
6
7
  [K in keyof T]: T[K];
7
8
  } & {};
@@ -20,14 +21,14 @@ type SqlConfigBaseValue<TSql> = TSql extends {
20
21
  } ? number : TSql extends {
21
22
  type: "date" | "datetime" | "timestamp";
22
23
  } ? Date : TSql extends {
23
- type: "varchar" | "char" | "text" | "longtext";
24
+ type: "varchar" | "char" | "text" | "longtext" | "enum";
24
25
  } ? string : unknown;
25
26
  type SqlOnlyValue<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
26
27
  nullable: true;
27
28
  } ? SqlConfigBaseValue<TSql> | null : SqlConfigBaseValue<TSql> : unknown;
28
29
  type IsSqlOnlyField<TField> = SqlConfigOf<TField> extends infer TSql ? TSql extends {
29
- sqlOnly?: infer TSqlOnly;
30
- } ? true extends TSqlOnly ? true : false : false : false;
30
+ sqlOnly: true;
31
+ } ? true : false : false;
31
32
  type IsOptionalSqlOnly<TField> = TField extends {
32
33
  config: {
33
34
  sql: {
@@ -47,27 +48,53 @@ type IsOptionalSqlOnly<TField> = TField extends {
47
48
  };
48
49
  };
49
50
  } ? true : false;
51
+ type IsDerivedDbField<TTable, TKey> = TTable extends {
52
+ rawSchema: {
53
+ __derives?: {
54
+ forDb?: infer TForDb;
55
+ };
56
+ };
57
+ } ? TKey extends keyof NonNullable<TForDb> ? true : false : TTable extends {
58
+ deriveDependencies: infer TDerives;
59
+ } ? TKey extends keyof TDerives ? true : false : false;
50
60
  type SqlOnlyInput<T> = T extends {
51
61
  definition: infer TDefinition;
52
62
  } ? Prettify<{
53
- [K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? never : K : never]: SqlOnlyValue<TDefinition[K]>;
63
+ [K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
64
+ __type: "reference";
65
+ } ? never : IsDerivedDbField<T, K> extends true ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? never : K : never]: SqlOnlyValue<TDefinition[K]>;
54
66
  } & {
55
- [K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? K : never : never]?: SqlOnlyValue<TDefinition[K]>;
67
+ [K in keyof TDefinition as IsSqlOnlyField<TDefinition[K]> extends true ? K extends SchemaMetaKey ? never : TDefinition[K] extends {
68
+ __type: "reference";
69
+ } ? never : IsOptionalSqlOnly<TDefinition[K]> extends true ? K : never : never]?: SqlOnlyValue<TDefinition[K]>;
56
70
  }> : Record<string, never>;
71
+ type DbApiFor<T> = T extends {
72
+ transforms: {
73
+ parseForDb: (...args: any[]) => any;
74
+ parseFromDb: (...args: any[]) => any;
75
+ };
76
+ } ? TableDBApi<Row<Return<T["transforms"]["parseFromDb"]>>, Row<FirstArg<T["transforms"]["parseForDb"]>>, SqlOnlyInput<T>> : never;
77
+ type ConnectedView<T> = T extends {
78
+ transforms: {
79
+ parseForDb: (...args: any[]) => any;
80
+ parseFromDb: (...args: any[]) => any;
81
+ };
82
+ } ? Omit<T, keyof DbApiFor<T>> & DbApiFor<T> : T;
83
+ type ConnectedCreateView<T> = T extends {
84
+ createView: (...args: infer TArgs) => infer TView;
85
+ } ? {
86
+ createView: (...args: TArgs) => ConnectedView<TView>;
87
+ } : {};
57
88
  type ConnectedTable<T> = T extends {
58
89
  transforms: {
59
90
  parseForDb: (...args: any[]) => any;
60
91
  parseFromDb: (...args: any[]) => any;
61
92
  };
62
- } ? T & {
63
- db: TableDB<Return<T["transforms"]["parseFromDb"]>, FirstArg<T["transforms"]["parseForDb"]>, SqlOnlyInput<T>>;
64
- } : T;
93
+ } ? Omit<T, "createView" | keyof DbApiFor<T>> & DbApiFor<T> & ConnectedCreateView<T> : T;
65
94
  type ConnectedBox<T extends Record<string, unknown>> = {
66
95
  [K in keyof T]: ConnectedTable<T[K]>;
67
96
  } & {
68
- db: {
69
- transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
70
- };
97
+ transaction: <R>(fn: (txBox: ConnectedBox<T>) => Promise<R>) => Promise<R>;
71
98
  };
72
- export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<unknown>): ConnectedBox<T>;
99
+ export declare function connect<T extends Record<string, unknown>>(box: T, db: Kysely<any>): ConnectedBox<T>;
73
100
  export {};
@@ -91,8 +91,10 @@ function enhanceTable(entry, meta, db) {
91
91
  });
92
92
  return new Proxy(entry, {
93
93
  get(target, prop, receiver) {
94
- if (prop === "db")
95
- return tableDb;
94
+ if (prop in tableDb) {
95
+ const value = Reflect.get(tableDb, prop, tableDb);
96
+ return typeof value === "function" ? value.bind(tableDb) : value;
97
+ }
96
98
  return Reflect.get(target, prop, receiver);
97
99
  },
98
100
  });
@@ -208,8 +210,10 @@ export function connect(box, db) {
208
210
  }, reconcile, hydrateRow);
209
211
  return new Proxy(view, {
210
212
  get(target, prop, receiver) {
211
- if (prop === "db")
212
- return viewDb;
213
+ if (prop in viewDb) {
214
+ const value = Reflect.get(viewDb, prop, viewDb);
215
+ return typeof value === "function" ? value.bind(viewDb) : value;
216
+ }
213
217
  return Reflect.get(target, prop, receiver);
214
218
  },
215
219
  });
@@ -226,6 +230,6 @@ export function connect(box, db) {
226
230
  return fn(txBox);
227
231
  });
228
232
  };
229
- result.db = { transaction };
233
+ result.transaction = transaction;
230
234
  return result;
231
235
  }
@@ -1,2 +1,2 @@
1
1
  import { Kysely } from "kysely";
2
- export declare function createSqliteDb(path: string): Promise<Kysely<unknown>>;
2
+ export declare function createSqliteDb<TDb = unknown>(path: string): Promise<Kysely<TDb>>;
@@ -5,13 +5,14 @@ type RequiredKeys<T> = {
5
5
  [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
6
6
  }[keyof T];
7
7
  type InsertDbOnlyArgs<T extends Record<string, unknown>> = keyof T extends never ? [] : RequiredKeys<T> extends never ? [dbOnlyData?: Partial<T>] : [dbOnlyData: T];
8
+ export type TableDBApi<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> = Pick<TableDB<TClient, TCreate, TDbOnly>, "findMany" | "findById" | "byId" | "insert" | "create" | "update" | "delete" | "count" | "reconcileIds">;
8
9
  export declare class TableDB<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> {
9
10
  private db;
10
11
  private meta;
11
12
  private transforms;
12
13
  private reconcile?;
13
14
  private hydrateRow?;
14
- constructor(db: Kysely<unknown>, meta: TableMeta, transforms: {
15
+ constructor(db: Kysely<any>, meta: TableMeta, transforms: {
15
16
  toClient: (row: Record<string, unknown>) => TClient;
16
17
  toDb: (row: Record<string, unknown>) => Record<string, unknown>;
17
18
  parseForDb: (data: Record<string, unknown>) => Record<string, unknown>;
@@ -22,6 +23,13 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
22
23
  }) | undefined, hydrateRow?: ((row: Record<string, unknown>) => Promise<Record<string, unknown>>) | undefined);
23
24
  findMany(opts?: FindManyOpts<TClient>): Promise<TClient[]>;
24
25
  findById(id: unknown): Promise<TClient | null>;
26
+ byId(id: unknown): {
27
+ find: () => Promise<TClient | null>;
28
+ update: (data: Partial<TCreate>, dbOnlyData?: DbOnlyArg<TDbOnly>) => ReturnType<TableDB<TClient, TCreate, TDbOnly>["update"]>;
29
+ delete: () => Promise<{
30
+ deleted: boolean;
31
+ }>;
32
+ };
25
33
  insert(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): {
26
34
  ids: () => Promise<Record<string, unknown>>;
27
35
  full: () => Promise<TClient>;
@@ -39,7 +47,7 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
39
47
  private pickDbPatchFields;
40
48
  private isWritableDbColumn;
41
49
  private parseDbOnlyData;
42
- reconcileIds(clientData: unknown, ids: unknown): unknown;
50
+ reconcileIds<TData>(clientData: TData, ids: unknown): TData;
43
51
  private reconcileFlatIds;
44
52
  private mapIdsToClientFields;
45
53
  private clientKeyForDbField;
@@ -62,6 +62,13 @@ export class TableDB {
62
62
  const hydratedRow = this.hydrateRow ? await this.hydrateRow(row) : row;
63
63
  return this.transforms.parseFromDb(hydratedRow);
64
64
  }
65
+ byId(id) {
66
+ return {
67
+ find: () => this.findById(id),
68
+ update: (data, dbOnlyData) => this.update(id, data, dbOnlyData),
69
+ delete: () => this.delete(id),
70
+ };
71
+ }
65
72
  insert(data, ...args) {
66
73
  const dbOnlyData = args[0];
67
74
  return {
@@ -1,14 +1,18 @@
1
- export type WhereValue<T> = T | {
2
- contains?: string;
3
- startsWith?: string;
4
- endsWith?: string;
1
+ type ComparableWhereValue<T> = {
5
2
  gt?: T;
6
3
  gte?: T;
7
4
  lt?: T;
8
5
  lte?: T;
9
- in?: T[];
10
- not?: T | Exclude<WhereValue<T>, T>;
11
6
  };
7
+ type StringWhereValue<T> = Extract<T, string> extends never ? {} : {
8
+ contains?: string;
9
+ startsWith?: string;
10
+ endsWith?: string;
11
+ };
12
+ export type WhereValue<T> = T | ({
13
+ in?: Exclude<T, undefined>[];
14
+ not?: T;
15
+ } & ComparableWhereValue<T> & StringWhereValue<T>);
12
16
  export type WhereInput<T> = {
13
17
  [K in keyof T]?: WhereValue<T[K]>;
14
18
  };
@@ -37,3 +41,4 @@ export interface TableMeta {
37
41
  sqlOnlyValidators: Map<string, (val: unknown) => unknown>;
38
42
  deriveDependencies: Map<string, string[]>;
39
43
  }
44
+ export {};
@@ -1,14 +1,4 @@
1
1
  import fs from "fs/promises";
2
- const sqlTypeMap = {
3
- int: "INTEGER",
4
- varchar: (length = 255) => `VARCHAR(${length})`,
5
- char: (length = 1) => `CHAR(${length})`,
6
- text: "TEXT",
7
- longtext: "LONGTEXT",
8
- boolean: "TINYINT(1)",
9
- date: "DATE",
10
- datetime: "DATETIME",
11
- };
12
2
  function isWrappedSchema(input) {
13
3
  return (input !== null &&
14
4
  typeof input === "object" &&
@@ -16,6 +6,107 @@ function isWrappedSchema(input) {
16
6
  input.schemas !== null &&
17
7
  typeof input.schemas === "object");
18
8
  }
9
+ function escapeSqlString(value) {
10
+ return value.replace(/'/g, "''");
11
+ }
12
+ function quoteEnumValues(values) {
13
+ return values.map((value) => `'${escapeSqlString(value)}'`).join(", ");
14
+ }
15
+ function columnName(fieldName, sqlConfig) {
16
+ return sqlConfig.field ?? fieldName;
17
+ }
18
+ function assertDialect(current, next, tableName) {
19
+ if (current && current !== next) {
20
+ throw new Error(`Mixed SQL dialects in table "${tableName}": "${current}" and "${next}".`);
21
+ }
22
+ return next;
23
+ }
24
+ function sqlType(dialect, fieldName, tableName, config) {
25
+ switch (dialect) {
26
+ case "sqlite":
27
+ switch (config.type) {
28
+ case "int":
29
+ return "INTEGER";
30
+ case "boolean":
31
+ return "INTEGER";
32
+ case "varchar":
33
+ case "char":
34
+ case "text":
35
+ case "longtext":
36
+ case "enum":
37
+ return "TEXT";
38
+ case "date":
39
+ case "datetime":
40
+ case "timestamp":
41
+ return "TEXT";
42
+ }
43
+ break;
44
+ case "postgres":
45
+ switch (config.type) {
46
+ case "int":
47
+ return "INTEGER";
48
+ case "boolean":
49
+ return "BOOLEAN";
50
+ case "varchar":
51
+ return `VARCHAR(${config.length ?? 255})`;
52
+ case "char":
53
+ return `CHAR(${config.length ?? 1})`;
54
+ case "text":
55
+ case "longtext":
56
+ return "TEXT";
57
+ case "enum":
58
+ if (!config.name) {
59
+ throw new Error(`Postgres enum field "${tableName}.${fieldName}" requires a name.`);
60
+ }
61
+ return config.name;
62
+ case "date":
63
+ return "DATE";
64
+ case "datetime":
65
+ case "timestamp":
66
+ return "TIMESTAMP";
67
+ }
68
+ break;
69
+ case "mysql":
70
+ switch (config.type) {
71
+ case "int":
72
+ return "INTEGER";
73
+ case "boolean":
74
+ return "TINYINT(1)";
75
+ case "varchar":
76
+ return `VARCHAR(${config.length ?? 255})`;
77
+ case "char":
78
+ return `CHAR(${config.length ?? 1})`;
79
+ case "text":
80
+ return "TEXT";
81
+ case "longtext":
82
+ return "LONGTEXT";
83
+ case "enum":
84
+ return `ENUM(${quoteEnumValues(config.values ?? [])})`;
85
+ case "date":
86
+ return "DATE";
87
+ case "datetime":
88
+ return "DATETIME";
89
+ case "timestamp":
90
+ return "TIMESTAMP";
91
+ }
92
+ break;
93
+ }
94
+ throw new Error(`Unknown ${dialect} SQL type "${config.type}" for field "${tableName}.${fieldName}".`);
95
+ }
96
+ function defaultSql(value) {
97
+ if (value === "CURRENT_TIMESTAMP")
98
+ return "CURRENT_TIMESTAMP";
99
+ if (typeof value === "string")
100
+ return `'${escapeSqlString(value)}'`;
101
+ if (value instanceof Date)
102
+ return `'${value.toISOString()}'`;
103
+ return String(value);
104
+ }
105
+ function enumCheck(dialect, fieldName, config) {
106
+ if (dialect !== "sqlite" || config.type !== "enum")
107
+ return undefined;
108
+ return `CHECK (${fieldName} IN (${quoteEnumValues(config.values ?? [])}))`;
109
+ }
19
110
  export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", options = { includeForeignKeys: true }) {
20
111
  if (!input) {
21
112
  throw new Error("No schema input provided");
@@ -24,7 +115,8 @@ export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", o
24
115
  if (!schemas || typeof schemas !== "object") {
25
116
  throw new Error("Invalid schemas input");
26
117
  }
27
- const sql = [];
118
+ const statements = [];
119
+ const postgresEnums = new Map();
28
120
  for (const [name, schema] of Object.entries(schemas)) {
29
121
  const tableName = schema._tableName;
30
122
  if (!tableName) {
@@ -33,91 +125,80 @@ export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", o
33
125
  }
34
126
  const fields = [];
35
127
  const foreignKeys = [];
128
+ let tableDialect;
36
129
  for (const [fieldName, field] of Object.entries(schema)) {
37
- // Skip metadata fields
38
- const f = field; // Just cast once
39
- console.log(`Processing field: ${fieldName}`, f);
40
- // Skip metadata fields
130
+ const f = field;
41
131
  if (fieldName === "_tableName" ||
42
132
  fieldName === "SchemaWrapperBrand" ||
43
133
  fieldName.startsWith("__") ||
44
134
  typeof f !== "object" ||
45
- !f)
135
+ !f) {
46
136
  continue;
47
- // Handle reference fields
137
+ }
48
138
  if (f.type === "reference" && f.to) {
49
139
  const referencedField = f.to();
50
140
  const targetTableName = referencedField.__parentTableType._tableName;
51
141
  const targetFieldName = referencedField.__meta._key;
52
- console.log(`Found reference field: ${fieldName} -> ${targetTableName}.${targetFieldName}`);
53
142
  fields.push(` ${fieldName} INTEGER NOT NULL`);
54
143
  if (options.includeForeignKeys) {
55
144
  foreignKeys.push(` FOREIGN KEY (${fieldName}) REFERENCES ${targetTableName}(${targetFieldName})`);
56
145
  }
57
146
  continue;
58
147
  }
59
- // Get the actual field definition from enriched structure
60
- let fieldDef = f;
61
- // If it's an enriched field, extract the original field definition
62
- if (f.__meta && f.__meta._fieldType) {
63
- fieldDef = f.__meta._fieldType;
64
- }
65
- // Now check if fieldDef has config
66
- if (fieldDef && fieldDef.config && fieldDef.config.sql) {
67
- const sqlConfig = fieldDef.config.sql;
68
- // Handle relation configs (hasMany, hasOne, etc.)
69
- if (["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
70
- // Only belongsTo creates a column
71
- if (sqlConfig.type === "belongsTo" &&
72
- sqlConfig.fromKey &&
73
- sqlConfig.schema) {
74
- fields.push(` ${sqlConfig.fromKey} INTEGER`);
75
- if (options.includeForeignKeys) {
76
- const targetSchema = sqlConfig.schema();
77
- foreignKeys.push(` FOREIGN KEY (${sqlConfig.fromKey}) REFERENCES ${targetSchema._tableName}(id)`);
78
- }
148
+ const fieldDef = f.__meta?._fieldType ?? f;
149
+ const sqlConfig = fieldDef?.config?.sql;
150
+ if (!sqlConfig)
151
+ continue;
152
+ if (["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
153
+ if (sqlConfig.type === "belongsTo" &&
154
+ sqlConfig.fromKey &&
155
+ sqlConfig.schema) {
156
+ fields.push(` ${sqlConfig.fromKey} INTEGER`);
157
+ if (options.includeForeignKeys) {
158
+ const targetSchema = sqlConfig.schema();
159
+ foreignKeys.push(` FOREIGN KEY (${sqlConfig.fromKey}) REFERENCES ${targetSchema._tableName}(id)`);
79
160
  }
80
- continue;
81
- }
82
- // Handle regular SQL types
83
- const { type, nullable, pk, length, default: defaultValue } = sqlConfig;
84
- if (!sqlTypeMap[type]) {
85
- console.warn(`Unknown SQL type: ${type} for field ${fieldName}`);
86
- continue;
87
- }
88
- const sqlType = typeof sqlTypeMap[type] === "function"
89
- ? sqlTypeMap[type](length)
90
- : sqlTypeMap[type];
91
- let fieldDefStr = ` ${fieldName} ${sqlType}`;
92
- if (pk)
93
- fieldDefStr += " PRIMARY KEY AUTO_INCREMENT";
94
- if (!nullable && !pk)
95
- fieldDefStr += " NOT NULL";
96
- // Handle defaults
97
- if (defaultValue !== undefined &&
98
- defaultValue !== "CURRENT_TIMESTAMP") {
99
- fieldDefStr += ` DEFAULT ${typeof defaultValue === "string" ? `'${defaultValue}'` : defaultValue}`;
100
161
  }
101
- else if (defaultValue === "CURRENT_TIMESTAMP") {
102
- fieldDefStr += " DEFAULT CURRENT_TIMESTAMP";
103
- }
104
- fields.push(fieldDefStr);
162
+ continue;
163
+ }
164
+ const dialect = sqlConfig.dialect;
165
+ if (!dialect) {
166
+ throw new Error(`Field "${tableName}.${fieldName}" is missing a SQL dialect.`);
167
+ }
168
+ tableDialect = assertDialect(tableDialect, dialect, tableName);
169
+ if (dialect === "postgres" && sqlConfig.type === "enum") {
170
+ postgresEnums.set(sqlConfig.name, sqlConfig.values);
171
+ }
172
+ const dbFieldName = columnName(fieldName, sqlConfig);
173
+ const parts = [
174
+ dbFieldName,
175
+ sqlType(dialect, fieldName, tableName, sqlConfig),
176
+ ];
177
+ if (sqlConfig.pk) {
178
+ parts.push(dialect === "mysql" ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY");
179
+ }
180
+ if (!sqlConfig.nullable && !sqlConfig.pk)
181
+ parts.push("NOT NULL");
182
+ if (sqlConfig.default !== undefined) {
183
+ parts.push(`DEFAULT ${defaultSql(sqlConfig.default)}`);
105
184
  }
185
+ const check = enumCheck(dialect, dbFieldName, sqlConfig);
186
+ if (check)
187
+ parts.push(check);
188
+ fields.push(` ${parts.join(" ")}`);
106
189
  }
107
- // Combine fields and foreign keys based on option
108
190
  const allFields = options.includeForeignKeys
109
191
  ? [...fields, ...foreignKeys]
110
192
  : fields;
111
- // Create table SQL
112
193
  if (allFields.length > 0) {
113
- sql.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);\n`);
194
+ statements.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);`);
114
195
  }
115
196
  else {
116
197
  console.warn(`Warning: Table ${tableName} has no fields`);
117
198
  }
118
199
  }
119
- // Write to file
120
- const sqlContent = sql.join("\n");
200
+ const enumStatements = Array.from(postgresEnums.entries()).map(([name, values]) => `CREATE TYPE ${name} AS ENUM (${quoteEnumValues(values)});`);
201
+ const sqlContent = [...enumStatements, ...statements].join("\n\n");
121
202
  await fs.writeFile(outputPath, sqlContent, "utf-8");
122
203
  return sqlContent;
123
204
  }
package/dist/schema.d.ts CHANGED
@@ -9,7 +9,8 @@ type CurrentTimestampConfig = {
9
9
  export declare const isFunction: (fn: unknown) => fn is Function;
10
10
  export declare function currentTimeStamp(): CurrentTimestampConfig;
11
11
  type DbConfig = SQLType | RelationConfig<any> | null;
12
- export type SQLType = ({
12
+ export type SQLDialect = "sqlite" | "postgres" | "mysql";
13
+ type SQLTypeConfig = ({
13
14
  type: "int";
14
15
  nullable?: boolean;
15
16
  default?: number;
@@ -31,16 +32,33 @@ export type SQLType = ({
31
32
  nullable?: boolean;
32
33
  length?: number;
33
34
  default?: string;
35
+ } | {
36
+ type: "enum";
37
+ values: readonly [string, ...string[]];
38
+ nullable?: boolean;
39
+ default?: string;
40
+ name?: string;
34
41
  }) & BaseConfig;
42
+ export type SQLType = SQLTypeConfig & {
43
+ dialect: SQLDialect;
44
+ };
45
+ type SQLTypeInput = SQLTypeConfig;
46
+ type WithDialect<T extends SQLTypeInput, TDialect extends SQLDialect> = SQLType & T & {
47
+ dialect: TDialect;
48
+ };
35
49
  type BaseConfig = {
36
50
  nullable?: boolean;
37
51
  pk?: true;
38
52
  field?: string;
39
53
  sqlOnly?: true;
40
54
  };
41
- type SQLToZodType<T extends SQLType, TDefault extends boolean> = T["pk"] extends true ? TDefault extends true ? z.ZodString : z.ZodNumber : T["nullable"] extends true ? T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodNullable<z.ZodString> : T["type"] extends "int" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "boolean" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
55
+ type SQLToZodType<T extends SQLTypeInput, TDefault extends boolean> = T["pk"] extends true ? TDefault extends true ? z.ZodString : z.ZodNumber : T["nullable"] extends true ? T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodNullable<z.ZodString> : T["type"] extends "enum" ? T extends {
56
+ values: infer TValues extends readonly [string, ...string[]];
57
+ } ? z.ZodNullable<z.ZodType<TValues[number]>> : never : T["type"] extends "int" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "boolean" ? z.ZodNullable<z.ZodNumber> : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
42
58
  default: "CURRENT_TIMESTAMP";
43
- } ? TDefault extends true ? never : z.ZodNullable<z.ZodDate> : z.ZodNullable<z.ZodDate> : never : T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodString : T["type"] extends "int" ? z.ZodNumber : T["type"] extends "boolean" ? z.ZodNumber : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
59
+ } ? TDefault extends true ? never : z.ZodNullable<z.ZodDate> : z.ZodNullable<z.ZodDate> : never : T["type"] extends "varchar" | "char" | "text" | "longtext" ? z.ZodString : T["type"] extends "enum" ? T extends {
60
+ values: infer TValues extends readonly [string, ...string[]];
61
+ } ? z.ZodType<TValues[number]> : never : T["type"] extends "int" ? z.ZodNumber : T["type"] extends "boolean" ? z.ZodNumber : T["type"] extends "date" | "datetime" | "timestamp" ? T extends {
44
62
  default: "CURRENT_TIMESTAMP";
45
63
  } ? TDefault extends true ? never : z.ZodDate : z.ZodDate : never;
46
64
  type ZodTypeFromPrimitive<T> = T extends string ? z.ZodString : T extends number ? z.ZodNumber : T extends boolean ? z.ZodBoolean : T extends Date ? z.ZodDate : z.ZodAny;
@@ -164,7 +182,9 @@ interface ShapeAPI {
164
182
  clientInput: <const TValue>(value: TValue | ((tools: {
165
183
  uuid: () => string;
166
184
  }) => TValue)) => Builder<"clientInput", null, z.ZodUndefined, TValue extends () => infer R ? R : TValue, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>;
167
- sql: <const T extends SQLType>(sqlConfig: T) => Builder<"sql", T, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
185
+ sqlite: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "sqlite">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
186
+ postgres: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "postgres">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
187
+ mysql: <const T extends SQLTypeInput>(sqlConfig: T) => Builder<"sql", WithDialect<T, "mysql">, SQLToZodType<T, false>, z.infer<SQLToZodType<T, false>>, SQLToZodType<T, false>, SQLToZodType<T, false>>;
168
188
  reference: <TGetter extends () => any>(getter: TGetter) => Reference<TGetter>;
169
189
  hasMany: <T extends HasManyDefault>(config?: T) => PlaceholderRelation<"hasMany">;
170
190
  hasOne: (config?: HasOneDefault) => PlaceholderRelation<"hasOne">;
@@ -245,7 +265,7 @@ export type Schema<T extends Record<string, SchemaField | (() => Relation<any>)>
245
265
  __schemaId?: string;
246
266
  [key: string]: T[keyof T] | string | ((id: number) => string) | true | undefined;
247
267
  };
248
- type ValidShapeField = ReturnType<typeof s.sql>;
268
+ type ValidShapeField = ReturnType<typeof s.sqlite> | ReturnType<typeof s.postgres> | ReturnType<typeof s.mysql>;
249
269
  export type ShapeSchema<T extends string = string> = {
250
270
  _tableName: T;
251
271
  [SchemaWrapperBrand]?: true;
package/dist/schema.js CHANGED
@@ -7,6 +7,48 @@ export function currentTimeStamp() {
7
7
  defaultValue: new Date(),
8
8
  };
9
9
  }
10
+ function createSqlBuilder(dialect, sqlConfig) {
11
+ const sqlZodType = (() => {
12
+ let baseType;
13
+ if (sqlConfig.pk) {
14
+ baseType = z.number();
15
+ }
16
+ else {
17
+ switch (sqlConfig.type) {
18
+ case "int":
19
+ baseType = z.number();
20
+ break;
21
+ case "boolean":
22
+ baseType = z.number();
23
+ break;
24
+ case "date":
25
+ case "datetime":
26
+ case "timestamp":
27
+ baseType = z.date();
28
+ break;
29
+ case "enum":
30
+ baseType = z.enum(sqlConfig.values);
31
+ break;
32
+ default:
33
+ baseType = z.string();
34
+ break;
35
+ }
36
+ }
37
+ if (sqlConfig.nullable) {
38
+ baseType = baseType.nullable();
39
+ }
40
+ return baseType;
41
+ })();
42
+ const dialectConfig = { ...sqlConfig, dialect };
43
+ return createBuilder({
44
+ stage: "sql",
45
+ sqlConfig: dialectConfig,
46
+ sqlZod: sqlZodType,
47
+ initialValue: inferDefaultFromZod(sqlZodType, dialectConfig),
48
+ clientZod: sqlZodType,
49
+ validationZod: sqlZodType,
50
+ });
51
+ }
10
52
  export const s = {
11
53
  clientInput: (value) => {
12
54
  const sample = isFunction(value) ? value({ uuid }) : value;
@@ -60,44 +102,9 @@ export const s = {
60
102
  relationType: "manyToMany",
61
103
  defaultCount: config?.defaultCount ?? 0,
62
104
  }),
63
- sql: (sqlConfig) => {
64
- const sqlZodType = (() => {
65
- let baseType;
66
- if (sqlConfig.pk) {
67
- baseType = z.number();
68
- }
69
- else {
70
- switch (sqlConfig.type) {
71
- case "int":
72
- baseType = z.number();
73
- break;
74
- case "boolean":
75
- baseType = z.number();
76
- break;
77
- case "date":
78
- case "datetime":
79
- case "timestamp":
80
- baseType = z.date();
81
- break;
82
- default:
83
- baseType = z.string();
84
- break;
85
- }
86
- }
87
- if (sqlConfig.nullable) {
88
- baseType = baseType.nullable();
89
- }
90
- return baseType;
91
- })();
92
- return createBuilder({
93
- stage: "sql",
94
- sqlConfig: sqlConfig,
95
- sqlZod: sqlZodType,
96
- initialValue: inferDefaultFromZod(sqlZodType, sqlConfig),
97
- clientZod: sqlZodType,
98
- validationZod: sqlZodType,
99
- });
100
- },
105
+ sqlite: (sqlConfig) => createSqlBuilder("sqlite", sqlConfig),
106
+ postgres: (sqlConfig) => createSqlBuilder("postgres", sqlConfig),
107
+ mysql: (sqlConfig) => createSqlBuilder("mysql", sqlConfig),
101
108
  };
102
109
  function createBuilder(config) {
103
110
  const completedStages = config.completedStages || new Set([config.stage]);
@@ -387,6 +394,8 @@ function inferDefaultFromZod(zodType, sqlConfig) {
387
394
  case "char":
388
395
  case "longtext":
389
396
  return "";
397
+ case "enum":
398
+ return sqlTypeConfig.default ?? sqlTypeConfig.values[0];
390
399
  case "int":
391
400
  return 0;
392
401
  case "boolean":
@@ -633,7 +642,7 @@ export function createSchema(schema, relations) {
633
642
  // 2. Map Database ONLY derives directly to the dbObject
634
643
  if (derives?.forDb) {
635
644
  for (const schemaKey in derives.forDb) {
636
- // Resolve custom DB column name if they used s.sql({ field: "custom_name" })
645
+ // Resolve custom DB column name if they used s.sqlite({ field: "custom_name" })
637
646
  const sqlConfig = fullSchema[schemaKey]?.config?.sql;
638
647
  const dbKey = sqlConfig?.field || schemaKey;
639
648
  dbObject[dbKey] = derives.forDb[schemaKey]?.(clientObject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.192",
3
+ "version": "0.5.193",
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",