cogsbox-shape 0.5.69 → 0.5.71

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
@@ -1,184 +1,234 @@
1
- # Unified Schema
1
+ # cogsbox-shape
2
2
 
3
- > ⚠️ **Warning**: This package is currently a work in progress and not ready for production use. The API is unstable and subject to breaking changes. Please do not use in production environments.
3
+ > [!CAUTION]
4
+ > **This library is under active development and the API is rapidly changing. Do not use in production.**
5
+ >
6
+ > Breaking changes are expected between any release. The library is currently in an experimental phase as we work towards a stable v1.0 release.
4
7
 
5
- ---
6
-
7
- 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.
8
-
9
- ## Features
10
-
11
- - Single source of truth for database, server, and client types
12
- - Type-safe schema definitions with TypeScript
13
- - Built-in Zod validation
14
- - Automatic type transformations between client and database
15
- - Relationship handling (hasMany, hasOne, belongsTo)
16
- - Schema serialization
17
- - Default value generation
8
+ A TypeScript-first schema declaration and validation library for full-stack applications. Define your database schema once and get type-safe schemas for your database, client, and validation layers with automatic transformations.
18
9
 
19
10
  ## Installation
20
11
 
21
12
  ```bash
22
13
  npm install cogsbox-shape
14
+ # or
15
+ yarn add cogsbox-shape
16
+ # or
17
+ pnpm add cogsbox-shape
23
18
  ```
24
19
 
25
- ## Basic Usage
20
+ ## The Problem
26
21
 
27
- ```typescript
28
- import { shape, hasMany, createSchema } from "cogsbox-shape";
22
+ In full-stack applications, data flows through multiple layers:
29
23
 
30
- const productSchema = {
31
- _tableName: "products",
32
- id: shape.sql({ type: "int", pk: true }),
33
- sku: shape
34
- .sql({ type: "varchar", length: 50 })
35
- .initialState(
36
- z.string(),
37
- () => "PRD-" + Math.random().toString(36).slice(2)
38
- )
39
- .validation(({ sql }) => sql.min(5).max(50)),
40
- price: shape
41
- .sql({ type: "int" })
42
- .client(({ sql }) => z.number().multipleOf(0.01))
43
- .transform({
44
- toClient: (dbValue) => dbValue / 100,
45
- toDb: (clientValue) => Math.round(clientValue * 100),
46
- }),
47
- inStock: shape
48
- .sql({ type: "boolean" })
49
- .client(({ sql }) => z.boolean())
50
- .initialState(z.boolean(), () => true),
51
- categories: hasMany({
52
- fromKey: "id",
53
- toKey: () => categorySchema.productId,
54
- schema: () => categorySchema,
55
- }),
56
- };
24
+ - **Database** stores data in SQL types (integers, varchars, etc.)
25
+ - **Server** needs to transform data for clients (e.g., convert cents to dollars)
26
+ - **Client** expects data in specific formats (e.g., UUIDs as strings, not numbers)
27
+ - **Forms** need validation rules and default values
28
+
29
+ Traditional approaches require defining these transformations in multiple places, leading to type mismatches and runtime errors.
30
+
31
+ ## The Solution: The Shape Flow
32
+
33
+ cogsbox-shape introduces a unified flow that mirrors how data moves through your application:
57
34
 
58
- const { sqlSchema, clientSchema, validationSchema, defaultValues } =
59
- createSchema(productSchema);
60
35
  ```
36
+ SQL → Initial State → Client → Validation
37
+ ```
38
+
39
+ This flow ensures type safety at every step while giving you control over transformations.
61
40
 
62
- ## Advanced Features
41
+ ## Core Concept: The Shape Flow
63
42
 
64
- ### Type Transformations
43
+ ### 1. SQL - Define Your Database Schema
65
44
 
66
- Transform data between client and database representations:
45
+ Start with your database reality:
67
46
 
68
47
  ```typescript
69
- const orderSchema = {
70
- _tableName: "orders",
71
- id: shape
72
- .sql({ type: "int", pk: true })
73
- .initialState(z.string().uuid(), () => crypto.randomUUID())
48
+ const userSchema = schema({
49
+ _tableName: "users",
50
+ id: s.int({ pk: true }), // In DB: integer auto-increment
51
+ email: s.varchar({ length: 255 }),
52
+ createdAt: s.datetime({ default: "CURRENT_TIMESTAMP" }),
53
+ });
54
+ ```
55
+
56
+ ### 2. Initial State - Define Creation Defaults
57
+
58
+ When creating new records, you often need different types than what's stored in the database. Initial state serves two purposes: defining default values AND adding additional types to the client schema.
59
+
60
+ ```typescript
61
+ const userSchema = schema({
62
+ _tableName: "users",
63
+ id: s
64
+ .int({ pk: true })
65
+ .initialState(z.string().uuid(), () => crypto.randomUUID()),
66
+ // DB stores integers, but client can work with UUID strings for new records
67
+ // This automatically creates a union type: number | string on the client
68
+ });
69
+ ```
70
+
71
+ ### 3. Client - Define Client Representation
72
+
73
+ Transform how data appears to clients:
74
+
75
+ ```typescript
76
+ const productSchema = schema({
77
+ _tableName: "products",
78
+ id: s
79
+ .int({ pk: true })
80
+ .initialState(z.string(), () => `tmp_${Date.now()}`)
74
81
  .client(({ sql, initialState }) => z.union([sql, initialState])),
75
- status: shape
76
- .sql({ type: "varchar", length: 20 })
77
- .client(({ sql }) =>
78
- z.enum(["pending", "processing", "shipped", "delivered"])
79
- )
80
- .validation(({ sql }) =>
81
- sql.refine((val) =>
82
- ["pending", "processing", "shipped", "delivered"].includes(val)
83
- )
84
- ),
85
- metadata: shape
86
- .sql({ type: "text" })
87
- .client(({ sql }) => z.record(z.unknown()))
88
- .transform({
89
- toClient: (value) => JSON.parse(value),
90
- toDb: (value) => JSON.stringify(value),
91
- }),
92
- createdAt: shape
93
- .sql({ type: "datetime" })
94
- .client(({ sql }) => z.string().datetime())
82
+ // Client can receive either the integer (from DB) or string (when creating)
83
+
84
+ price: s
85
+ .int() // Stored as cents in DB
86
+ .client(() => z.number().multipleOf(0.01)) // But dollars on client
95
87
  .transform({
96
- toClient: (date) => date.toISOString(),
97
- toDb: (isoString) => new Date(isoString),
88
+ toClient: (cents) => cents / 100,
89
+ toDb: (dollars) => Math.round(dollars * 100),
98
90
  }),
99
- };
91
+ });
100
92
  ```
101
93
 
102
- ### Relationships
94
+ ### 4. Validation - Define Business Rules
103
95
 
104
- Define relationships between schemas:
96
+ Add validation that runs before data enters your system:
105
97
 
106
98
  ```typescript
107
- const customerSchema = {
108
- _tableName: "customers",
109
- id: shape.sql({ type: "int", pk: true }),
110
- name: shape.sql({ type: "varchar", length: 100 }),
111
- orders: hasMany({
112
- fromKey: "id",
113
- toKey: () => orderSchema.customerId,
114
- schema: () => orderSchema,
115
- }),
116
- primaryAddress: hasOne({
117
- fromKey: "id",
118
- toKey: () => addressSchema.customerId,
119
- schema: () => addressSchema,
120
- }),
121
- company: belongsTo({
122
- fromKey: "companyId",
123
- toKey: () => companySchema.id,
124
- schema: () => companySchema,
125
- }),
126
- };
99
+ const userSchema = schema({
100
+ _tableName: "users",
101
+ email: s
102
+ .varchar({ length: 255 })
103
+ .client(({ sql }) => sql)
104
+ .validation(({ client }) => client.email().toLowerCase()),
105
+
106
+ age: s.int().validation(({ sql }) => sql.min(18).max(120)),
107
+ });
127
108
  ```
128
109
 
129
- ### SQL Types
110
+ ## Why This Flow?
130
111
 
131
- Built-in SQL type definitions:
112
+ The flow matches how data moves through your application:
113
+
114
+ 1. **SQL**: Database constraints and types
115
+ 2. **Initial State**: What shape new records take before persistence
116
+ 3. **Client**: How data looks in your UI/API
117
+ 4. **Validation**: Business rules applied to user input
118
+ 5. **Transform**: Convert between database and client representations
119
+
120
+ Each step can reference previous steps, creating a pipeline:
132
121
 
133
122
  ```typescript
134
- shape.int({ nullable: true });
135
- shape.varchar({ length: 255 });
136
- shape.boolean();
137
- shape.date();
138
- shape.datetime();
139
- shape.text();
140
- shape.longtext();
123
+ const orderSchema = schema({
124
+ _tableName: "orders",
125
+ status: s
126
+ .varchar({ length: 20 })
127
+ // 1. SQL: Simple varchar in database
128
+ .initialState(z.literal("draft"), () => "draft")
129
+ // 2. Initial: New orders start as 'draft'
130
+ .client(({ sql }) => z.enum(["draft", "pending", "shipped", "delivered"]))
131
+ // 3. Client: Enforce enum on client
132
+ .validation(({ client }) => client),
133
+ // 4. Validation: Use same rules as client
134
+
135
+ totalPrice: s
136
+ .int()
137
+ // 1. SQL: Store as cents (integer)
138
+ .client(() => z.number().multipleOf(0.01))
139
+ // 2. Client: Work with dollars (decimal)
140
+ .transform({
141
+ toClient: (cents) => cents / 100,
142
+ toDb: (dollars) => Math.round(dollars * 100),
143
+ }),
144
+ // 3. Transform: Automatically convert between cents and dollars
145
+ });
141
146
  ```
142
147
 
143
- ### Validation
148
+ This approach ensures type safety throughout your entire data lifecycle while keeping transformations co-located with your schema definition.
144
149
 
145
- Add Zod validation to your schemas:
150
+ ## Real-World Example
151
+
152
+ Here's a complete example showing the power of the flow:
146
153
 
147
154
  ```typescript
148
- const userSchema = {
155
+ const userSchema = schema({
149
156
  _tableName: "users",
150
- email: shape
151
- .sql({ type: "varchar", length: 255 })
152
- .validation(({ sql }) => sql.email().toLowerCase()),
153
- password: shape
154
- .sql({ type: "varchar", length: 255 })
155
- .validation(({ sql }) =>
156
- sql
157
- .min(8)
158
- .regex(/[A-Z]/, "Must contain uppercase letter")
159
- .regex(/[0-9]/, "Must contain number")
157
+ id: s.int({ pk: true }).initialState(z.string().uuid(), () => uuidv4()),
158
+
159
+ email: s.varchar({ length: 255 }).validation(({ sql }) => sql.email()),
160
+
161
+ metadata: s
162
+ .text()
163
+ .initialState(
164
+ z.object({
165
+ preferences: z.object({
166
+ theme: z.enum(["light", "dark"]),
167
+ notifications: z.boolean(),
168
+ }),
169
+ }),
170
+ () => ({ preferences: { theme: "light", notifications: true } })
171
+ )
172
+ .client(({ initialState }) => initialState)
173
+ .transform({
174
+ toClient: (json) => JSON.parse(json),
175
+ toDb: (obj) => JSON.stringify(obj),
176
+ }),
177
+ });
178
+
179
+ const userRelations = schemaRelations(userSchema, (rel) => ({
180
+ posts: rel
181
+ .hasMany({
182
+ fromKey: "id",
183
+ toKey: () => postRelations.userId,
184
+ defaultCount: 0,
185
+ })
186
+ .validation(({ client }) =>
187
+ client.min(1, "User must have at least one post")
160
188
  ),
161
- birthDate: shape
162
- .sql({ type: "date" })
163
- .validation(({ sql }) => sql.min(new Date("1900-01-01")).max(new Date())),
164
- };
189
+ }));
190
+
191
+ // Generate schemas
192
+ const { sqlSchema, clientSchema, validationSchema, defaultValues } =
193
+ createSchema(userSchema, userRelations);
194
+
195
+ // Use in your app
196
+ const newUser = defaultValues; // Fully typed with defaults
197
+ const validated = validationSchema.parse(userInput); // Runtime validation
198
+ const dbUser = toDb(validated); // Transform for database
199
+ const apiUser = toClient(dbUser); // Transform for API
165
200
  ```
166
201
 
167
- ## Type Safety
202
+ ## Relationships
168
203
 
169
- The library provides full type inference:
204
+ Define relationships that are type-safe across all layers:
170
205
 
171
206
  ```typescript
172
- const { sqlSchema, clientSchema, validationSchema, defaultValues } =
173
- createSchema(userSchema);
174
-
175
- // These are fully typed:
176
- type DBUser = z.infer<typeof sqlSchema>;
177
- type ClientUser = z.infer<typeof clientSchema>;
178
- type ValidationUser = z.infer<typeof validationSchema>;
179
- const defaults: typeof defaultValues = {
180
- // TypeScript will ensure this matches your schema
181
- };
207
+ const messageSchema = schema({
208
+ _tableName: "messages",
209
+ id: s.int({ pk: true }).initialState(z.string(), () => uuidv4()),
210
+ content: s.text(),
211
+ timestamp: s.datetime(),
212
+ });
213
+
214
+ const messageRelations = schemaRelations(messageSchema, (rel) => ({
215
+ recipients: rel
216
+ .hasMany({
217
+ fromKey: "id",
218
+ toKey: () => recipientRelations.messageId,
219
+ })
220
+ .validation(({ sql }) => sql.min(1, "Must have at least one recipient")),
221
+ }));
222
+
223
+ // The flow works with relationships too!
224
+ const { clientSchema } = createSchema(messageSchema, messageRelations);
225
+ type Message = z.infer<typeof clientSchema>;
226
+ // {
227
+ // id: string | number;
228
+ // content: string;
229
+ // timestamp: Date;
230
+ // recipients: Array<Recipient>;
231
+ // }
182
232
  ```
183
233
 
184
234
  ## License
package/dist/cli.js CHANGED
@@ -6,38 +6,48 @@ import { generateSQL } from "./generateSQL.js";
6
6
  import { unlink, writeFile } from "fs/promises";
7
7
  import { spawnSync } from "child_process";
8
8
  const program = new Command();
9
- program.command("generate-sql <file>").action(async (file) => {
9
+ program
10
+ .name("cogsbox-shape")
11
+ .description("CLI for cogsbox-shape schema tools")
12
+ .version("0.5.70");
13
+ program
14
+ .command("generate-sql <file>")
15
+ .description("Generate SQL from your schema definitions")
16
+ .option("-o, --output <path>", "Output SQL file path", "./cogsbox-shape-sql.sql")
17
+ .option("--no-foreign-keys", "Generate SQL without foreign key constraints")
18
+ .action(async (file, options) => {
10
19
  try {
11
20
  const fullPath = path.resolve(process.cwd(), file);
12
21
  if (file.endsWith(".ts")) {
13
- // Create a virtual module that imports and outputs the schema
22
+ // Create a virtual module that imports and USES the schema directly
14
23
  const virtualModule = `
15
- import { schemas } from '${pathToFileURL(fullPath).href}';
16
- console.log(JSON.stringify(schemas));
17
- `;
24
+ import { schemas } from '${pathToFileURL(fullPath).href}';
25
+ import { generateSQL } from '${pathToFileURL(path.join(process.cwd(), "dist/generateSQL.js")).href}';
26
+
27
+ generateSQL(schemas, '${options.output}', { includeForeignKeys: ${options.foreignKeys !== false} })
28
+ .then(() => console.log('Done'))
29
+ .catch(err => console.error(err));
30
+ `;
18
31
  // Write this to a temporary file
19
32
  const tmpFile = path.join(path.dirname(fullPath), ".tmp-schema-loader.ts");
20
33
  await writeFile(tmpFile, virtualModule, "utf8");
21
34
  const result = spawnSync("npx", ["tsx", tmpFile], {
22
35
  encoding: "utf8",
23
- stdio: ["inherit", "pipe", "pipe"],
36
+ stdio: "inherit",
24
37
  });
25
38
  // Clean up temp file
26
39
  await unlink(tmpFile).catch(() => { });
27
40
  if (result.error) {
28
41
  throw result.error;
29
42
  }
30
- if (result.stderr) {
31
- console.error("stderr:", result.stderr);
32
- }
33
- const schema = JSON.parse(result.stdout);
34
- await generateSQL(schema);
35
43
  }
36
44
  else {
37
45
  const schema = await import(pathToFileURL(fullPath).href);
38
- await generateSQL(schema.schemas);
46
+ await generateSQL(schema.schemas, options.output, {
47
+ includeForeignKeys: options.foreignKeys,
48
+ });
39
49
  }
40
- console.log("Generated SQL successfully");
50
+ console.log(`Generated SQL successfully at ${options.output}`);
41
51
  }
42
52
  catch (error) {
43
53
  console.error("Error:", error);
@@ -1,6 +1,7 @@
1
- import type { Schema } from "./schema";
2
- type SchemaInput = Record<string, Schema<any>> | {
3
- schemas: Record<string, Schema<any>>;
1
+ type SchemaInput = Record<string, any> | {
2
+ schemas: Record<string, any>;
4
3
  };
5
- export declare function generateSQL(input: SchemaInput, outputPath?: string): Promise<string>;
4
+ export declare function generateSQL(input: SchemaInput, outputPath?: string, options?: {
5
+ includeForeignKeys?: boolean;
6
+ }): Promise<string>;
6
7
  export {};
@@ -1,5 +1,4 @@
1
1
  import fs from "fs/promises";
2
- // SQL Type mapping
3
2
  const sqlTypeMap = {
4
3
  int: "INTEGER",
5
4
  varchar: (length = 255) => `VARCHAR(${length})`,
@@ -17,46 +16,105 @@ function isWrappedSchema(input) {
17
16
  input.schemas !== null &&
18
17
  typeof input.schemas === "object");
19
18
  }
20
- export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql") {
19
+ export async function generateSQL(input, outputPath = "cogsbox-shape-sql.sql", options = { includeForeignKeys: true }) {
21
20
  if (!input) {
22
21
  throw new Error("No schema input provided");
23
22
  }
24
- // Extract schemas using type guard
25
23
  const schemas = isWrappedSchema(input) ? input.schemas : input;
26
24
  if (!schemas || typeof schemas !== "object") {
27
25
  throw new Error("Invalid schemas input");
28
26
  }
29
27
  const sql = [];
30
- // Generate SQL for each schema
31
28
  for (const [name, schema] of Object.entries(schemas)) {
32
29
  const tableName = schema._tableName;
30
+ if (!tableName) {
31
+ console.warn(`Skipping schema '${name}' - no _tableName found`);
32
+ continue;
33
+ }
33
34
  const fields = [];
34
35
  const foreignKeys = [];
35
- // Process each field in the schema
36
36
  for (const [fieldName, field] of Object.entries(schema)) {
37
- if (fieldName === "_tableName")
37
+ // Skip metadata fields
38
+ const f = field; // Just cast once
39
+ console.log(`Processing field: ${fieldName}`, f);
40
+ // Skip metadata fields
41
+ if (fieldName === "_tableName" ||
42
+ fieldName === "SchemaWrapperBrand" ||
43
+ fieldName.startsWith("__") ||
44
+ typeof f !== "object" ||
45
+ !f)
46
+ continue;
47
+ // Handle reference fields
48
+ if (f.type === "reference" && f.to) {
49
+ const referencedField = f.to();
50
+ const targetTableName = referencedField.__parentTableType._tableName;
51
+ const targetFieldName = referencedField.__meta._key;
52
+ console.log(`Found reference field: ${fieldName} -> ${targetTableName}.${targetFieldName}`);
53
+ fields.push(` ${fieldName} INTEGER NOT NULL`);
54
+ if (options.includeForeignKeys) {
55
+ foreignKeys.push(` FOREIGN KEY (${fieldName}) REFERENCES ${targetTableName}(${targetFieldName})`);
56
+ }
38
57
  continue;
39
- // Handle regular fields
40
- if ("sql" in field) {
41
- const { type, nullable, pk, length } = field.sql;
58
+ }
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
+ }
79
+ }
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
+ }
42
88
  const sqlType = typeof sqlTypeMap[type] === "function"
43
89
  ? sqlTypeMap[type](length)
44
90
  : sqlTypeMap[type];
45
- fields.push(` ${fieldName} ${sqlType}${pk ? " PRIMARY KEY" : ""}${nullable ? "" : " NOT NULL"}`);
46
- }
47
- // Handle relations
48
- if (typeof field === "function") {
49
- const relation = field();
50
- if (relation.type === "belongsTo") {
51
- fields.push(` ${relation.fromKey} INTEGER`);
52
- foreignKeys.push(` FOREIGN KEY (${relation.fromKey}) REFERENCES ${relation.schema._tableName}(id)`);
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
+ }
101
+ else if (defaultValue === "CURRENT_TIMESTAMP") {
102
+ fieldDefStr += " DEFAULT CURRENT_TIMESTAMP";
53
103
  }
104
+ fields.push(fieldDefStr);
54
105
  }
55
106
  }
56
- // Combine fields and foreign keys
57
- const allFields = [...fields, ...foreignKeys];
107
+ // Combine fields and foreign keys based on option
108
+ const allFields = options.includeForeignKeys
109
+ ? [...fields, ...foreignKeys]
110
+ : fields;
58
111
  // Create table SQL
59
- sql.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);\n`);
112
+ if (allFields.length > 0) {
113
+ sql.push(`CREATE TABLE ${tableName} (\n${allFields.join(",\n")}\n);\n`);
114
+ }
115
+ else {
116
+ console.warn(`Warning: Table ${tableName} has no fields`);
117
+ }
60
118
  }
61
119
  // Write to file
62
120
  const sqlContent = sql.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.69",
3
+ "version": "0.5.71",
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",