cogsbox-shape 0.5.68 → 0.5.70

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.
Files changed (3) hide show
  1. package/README.md +188 -138
  2. package/dist/schema.d.ts +2 -2
  3. package/package.json +2 -2
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/schema.d.ts CHANGED
@@ -98,7 +98,7 @@ export type RelationConfig<T extends Schema<any>> = (BaseRelationConfig<T> & {
98
98
  type Stage = "sql" | "relation" | "new" | "client" | "validation" | "done";
99
99
  type StageMethods = {
100
100
  sql: "initialState" | "client" | "validation" | "transform";
101
- relation: "initialState" | "client" | "validation" | "transform";
101
+ relation: "validation" | "transform";
102
102
  new: "client" | "validation" | "transform";
103
103
  client: "validation" | "transform";
104
104
  validation: "transform";
@@ -307,7 +307,7 @@ export declare function schemaRelations<TSchema extends Schema<any>, RefObject e
307
307
  _key: K;
308
308
  _fieldType: RefObject[K];
309
309
  };
310
- __parentTableType: TSchema;
310
+ __parentTableType: TSchema & RefObject;
311
311
  };
312
312
  };
313
313
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.68",
3
+ "version": "0.5.70",
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",
@@ -49,7 +49,7 @@
49
49
  "prettier": "^3.1.1",
50
50
  "typescript": "^5.3.3",
51
51
  "vitest": "^3.2.0",
52
- "zod": "^3.25.67"
52
+ "zod": "^4.25.67"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "zod": "^3.22.4"