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.
- package/README.md +188 -138
- package/dist/schema.d.ts +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,184 +1,234 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cogsbox-shape
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
-
##
|
|
20
|
+
## The Problem
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
import { shape, hasMany, createSchema } from "cogsbox-shape";
|
|
22
|
+
In full-stack applications, data flows through multiple layers:
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
##
|
|
41
|
+
## Core Concept: The Shape Flow
|
|
63
42
|
|
|
64
|
-
###
|
|
43
|
+
### 1. SQL - Define Your Database Schema
|
|
65
44
|
|
|
66
|
-
|
|
45
|
+
Start with your database reality:
|
|
67
46
|
|
|
68
47
|
```typescript
|
|
69
|
-
const
|
|
70
|
-
_tableName: "
|
|
71
|
-
id:
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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: (
|
|
97
|
-
toDb: (
|
|
88
|
+
toClient: (cents) => cents / 100,
|
|
89
|
+
toDb: (dollars) => Math.round(dollars * 100),
|
|
98
90
|
}),
|
|
99
|
-
};
|
|
91
|
+
});
|
|
100
92
|
```
|
|
101
93
|
|
|
102
|
-
###
|
|
94
|
+
### 4. Validation - Define Business Rules
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
Add validation that runs before data enters your system:
|
|
105
97
|
|
|
106
98
|
```typescript
|
|
107
|
-
const
|
|
108
|
-
_tableName: "
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
110
|
+
## Why This Flow?
|
|
130
111
|
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
+
This approach ensures type safety throughout your entire data lifecycle while keeping transformations co-located with your schema definition.
|
|
144
149
|
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
##
|
|
202
|
+
## Relationships
|
|
168
203
|
|
|
169
|
-
|
|
204
|
+
Define relationships that are type-safe across all layers:
|
|
170
205
|
|
|
171
206
|
```typescript
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
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: "
|
|
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.
|
|
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": "^
|
|
52
|
+
"zod": "^4.25.67"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"zod": "^3.22.4"
|