@chidchanun/bcp 0.1.18 → 0.1.19
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/docs/README.md +30 -5
- package/docs/releases/0.1.19.md +106 -0
- package/docs/validation.md +342 -0
- package/package.json +5 -1
- package/packages/client/src/validation.ts +1118 -0
package/docs/README.md
CHANGED
|
@@ -4,7 +4,7 @@ This directory is the documentation source of truth for the BCP Framework docume
|
|
|
4
4
|
|
|
5
5
|
The recommended documentation website project name is `bcp-docs`.
|
|
6
6
|
|
|
7
|
-
> Documentation target: BCP Framework `0.1.
|
|
7
|
+
> Documentation target: BCP Framework `0.1.19`
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
10
10
|
|
|
@@ -40,6 +40,7 @@ The website should not invent API behavior that is not documented or tested in t
|
|
|
40
40
|
| `/docs/route-guards` | `route-guards.md` | Scoped page authorization |
|
|
41
41
|
| `/docs/form-actions` | `form-actions.md` | Server mutations and forms |
|
|
42
42
|
| `/docs/server-request-apis` | `server-request-apis.md` | Request, cookie, redirect and response APIs |
|
|
43
|
+
| `/docs/validation` | `validation.md` | Typed form/API validation and field errors |
|
|
43
44
|
|
|
44
45
|
### Authentication
|
|
45
46
|
|
|
@@ -72,9 +73,9 @@ Use the files under `docs/releases/` for release pages.
|
|
|
72
73
|
Recommended route format:
|
|
73
74
|
|
|
74
75
|
```text
|
|
75
|
-
/releases/0.1.16
|
|
76
76
|
/releases/0.1.17
|
|
77
77
|
/releases/0.1.18
|
|
78
|
+
/releases/0.1.19
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
The newest release should also be available from `/releases`.
|
|
@@ -102,6 +103,9 @@ Recommended feature groups:
|
|
|
102
103
|
- Form actions
|
|
103
104
|
- Request-scoped server APIs
|
|
104
105
|
- Cookies and redirects
|
|
106
|
+
- `bcp/validation`
|
|
107
|
+
- Typed form/API validation
|
|
108
|
+
- Field and form error output
|
|
105
109
|
|
|
106
110
|
### Database
|
|
107
111
|
|
|
@@ -153,7 +157,7 @@ Suggested content, not a required exact copy:
|
|
|
153
157
|
```text
|
|
154
158
|
BCP Framework
|
|
155
159
|
A React full-stack framework for building server-rendered applications with routing,
|
|
156
|
-
data loading, server mutations, authentication, databases, middleware and production tooling.
|
|
160
|
+
data loading, server mutations, authentication, databases, validation, middleware and production tooling.
|
|
157
161
|
```
|
|
158
162
|
|
|
159
163
|
Primary actions:
|
|
@@ -183,6 +187,7 @@ Core
|
|
|
183
187
|
Server Data Loaders
|
|
184
188
|
Route Guards
|
|
185
189
|
Form Actions
|
|
190
|
+
Validation
|
|
186
191
|
|
|
187
192
|
Authentication
|
|
188
193
|
Authentication Core
|
|
@@ -203,9 +208,9 @@ Production
|
|
|
203
208
|
Development Logging
|
|
204
209
|
|
|
205
210
|
Releases
|
|
211
|
+
0.1.19
|
|
206
212
|
0.1.18
|
|
207
213
|
0.1.17
|
|
208
|
-
0.1.16
|
|
209
214
|
```
|
|
210
215
|
|
|
211
216
|
## Suggested documentation page layout
|
|
@@ -249,6 +254,11 @@ import {
|
|
|
249
254
|
db,
|
|
250
255
|
} from "bcp/database";
|
|
251
256
|
|
|
257
|
+
import {
|
|
258
|
+
v,
|
|
259
|
+
validateFormData,
|
|
260
|
+
} from "bcp/validation";
|
|
261
|
+
|
|
252
262
|
import {
|
|
253
263
|
type MiddlewarePipelineHandler,
|
|
254
264
|
} from "bcp/middleware";
|
|
@@ -297,7 +307,7 @@ npx create-bcp-app@latest my-app
|
|
|
297
307
|
|
|
298
308
|
## Middleware v2 documentation priority
|
|
299
309
|
|
|
300
|
-
BCP 0.1.18
|
|
310
|
+
BCP 0.1.18 introduced a meaningful middleware architecture change, so the `bcp-docs` website should make the distinction between v1 and v2 clear.
|
|
301
311
|
|
|
302
312
|
The middleware page should prominently explain:
|
|
303
313
|
|
|
@@ -310,6 +320,21 @@ Existing v1 code remains supported, so migration documentation should describe v
|
|
|
310
320
|
|
|
311
321
|
See `middleware.md` for the full API and examples.
|
|
312
322
|
|
|
323
|
+
## Validation documentation priority
|
|
324
|
+
|
|
325
|
+
BCP 0.1.19 introduces the public `bcp/validation` entrypoint. The docs website should show validation in both form-action and API-route examples.
|
|
326
|
+
|
|
327
|
+
Important concepts to surface:
|
|
328
|
+
|
|
329
|
+
- `v.object`, `v.string`, `v.number`, `v.boolean`, `v.array`
|
|
330
|
+
- FormData coercion with `validateFormData()`
|
|
331
|
+
- `success/data` versus `issues/fieldErrors/formErrors`
|
|
332
|
+
- object fields are stripped unless `allowUnknown: true`
|
|
333
|
+
- `refine()` for custom application rules
|
|
334
|
+
- no external validation dependency is required
|
|
335
|
+
|
|
336
|
+
See `validation.md` for the complete API and examples.
|
|
337
|
+
|
|
313
338
|
## Release workflow for documentation
|
|
314
339
|
|
|
315
340
|
Before publishing a new BCP release:
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# BCP Framework 0.1.19
|
|
2
|
+
|
|
3
|
+
BCP Framework 0.1.19 adds the first built-in Validation System.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- new `bcp/validation` public entrypoint
|
|
8
|
+
- universal validation API for server and client code
|
|
9
|
+
- `v.object`, `v.string`, `v.number`, `v.boolean`, `v.literal`, `v.enum`, `v.array` and `v.union`
|
|
10
|
+
- `.optional()`, `.nullable()` and `.refine()` composition
|
|
11
|
+
- `safeParse()` and throwing `parse()` modes
|
|
12
|
+
- typed validation output through `InferValidator`
|
|
13
|
+
- `validateFormData()` for BCP form actions
|
|
14
|
+
- repeated FormData keys become arrays
|
|
15
|
+
- string/number/boolean coercion options for HTML forms
|
|
16
|
+
- field errors flattened into dot paths
|
|
17
|
+
- serializable validation failure objects
|
|
18
|
+
- unknown object fields are stripped by default
|
|
19
|
+
- no new external runtime dependency
|
|
20
|
+
|
|
21
|
+
## Form action example
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
validateFormData,
|
|
26
|
+
v,
|
|
27
|
+
} from "bcp/validation";
|
|
28
|
+
|
|
29
|
+
const schema =
|
|
30
|
+
v.object({
|
|
31
|
+
email:
|
|
32
|
+
v.string({
|
|
33
|
+
trim: true,
|
|
34
|
+
email: true,
|
|
35
|
+
}),
|
|
36
|
+
age:
|
|
37
|
+
v.number({
|
|
38
|
+
coerce: true,
|
|
39
|
+
min: 18,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export async function saveUser(
|
|
44
|
+
formData: FormData
|
|
45
|
+
) {
|
|
46
|
+
const result =
|
|
47
|
+
validateFormData(
|
|
48
|
+
schema,
|
|
49
|
+
formData
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
user: result.data,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API example
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const result =
|
|
67
|
+
schema.safeParse(
|
|
68
|
+
await request.json()
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
return Response.json(
|
|
73
|
+
result,
|
|
74
|
+
{
|
|
75
|
+
status: 422,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Error shape
|
|
82
|
+
|
|
83
|
+
Validation failures use the same shape in form actions, API routes and direct validation calls:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
{
|
|
87
|
+
success: false,
|
|
88
|
+
issues: [
|
|
89
|
+
{
|
|
90
|
+
path: ["email"],
|
|
91
|
+
message:
|
|
92
|
+
"Must be a valid email address.",
|
|
93
|
+
code:
|
|
94
|
+
"invalid_email",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
fieldErrors: {
|
|
98
|
+
email: [
|
|
99
|
+
"Must be a valid email address.",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
formErrors: [],
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See `docs/validation.md` for the full API.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
BCP Framework 0.1.19 adds a built-in validation system through `bcp/validation`.
|
|
4
|
+
|
|
5
|
+
The module is universal and can be used in server actions, API routes, loaders and client-side code.
|
|
6
|
+
|
|
7
|
+
## Basic schema
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
v,
|
|
12
|
+
} from "bcp/validation";
|
|
13
|
+
|
|
14
|
+
const userSchema =
|
|
15
|
+
v.object({
|
|
16
|
+
name:
|
|
17
|
+
v.string({
|
|
18
|
+
trim: true,
|
|
19
|
+
minLength: 2,
|
|
20
|
+
}),
|
|
21
|
+
email:
|
|
22
|
+
v.string({
|
|
23
|
+
trim: true,
|
|
24
|
+
email: true,
|
|
25
|
+
}),
|
|
26
|
+
age:
|
|
27
|
+
v.number({
|
|
28
|
+
coerce: true,
|
|
29
|
+
integer: true,
|
|
30
|
+
min: 18,
|
|
31
|
+
}),
|
|
32
|
+
role:
|
|
33
|
+
v.enum([
|
|
34
|
+
"admin",
|
|
35
|
+
"user",
|
|
36
|
+
]),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Safe parsing
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const result =
|
|
44
|
+
userSchema.safeParse(input);
|
|
45
|
+
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
console.log(
|
|
48
|
+
result.fieldErrors
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(result.data);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Successful results have:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
{
|
|
60
|
+
success: true,
|
|
61
|
+
data: value,
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Failures have:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
{
|
|
69
|
+
success: false,
|
|
70
|
+
issues: [...],
|
|
71
|
+
fieldErrors: {
|
|
72
|
+
email: [
|
|
73
|
+
"Must be a valid email address.",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
formErrors: [],
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Nested field paths are flattened with dot notation, for example `profile.email` and `items.0.name`.
|
|
81
|
+
|
|
82
|
+
## Form actions
|
|
83
|
+
|
|
84
|
+
`validateFormData()` converts repeated FormData keys into arrays and validates the resulting object.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
validateFormData,
|
|
89
|
+
v,
|
|
90
|
+
} from "bcp/validation";
|
|
91
|
+
|
|
92
|
+
const schema =
|
|
93
|
+
v.object({
|
|
94
|
+
email:
|
|
95
|
+
v.string({
|
|
96
|
+
trim: true,
|
|
97
|
+
email: true,
|
|
98
|
+
}),
|
|
99
|
+
age:
|
|
100
|
+
v.number({
|
|
101
|
+
coerce: true,
|
|
102
|
+
min: 18,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export async function saveUser(
|
|
107
|
+
formData: FormData
|
|
108
|
+
) {
|
|
109
|
+
const result =
|
|
110
|
+
validateFormData(
|
|
111
|
+
schema,
|
|
112
|
+
formData
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// result.data is typed and validated.
|
|
120
|
+
// await db.execute(...)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
user: result.data,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The failure object is serializable and can be returned from a BCP form action directly.
|
|
130
|
+
|
|
131
|
+
## API routes
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import {
|
|
135
|
+
v,
|
|
136
|
+
} from "bcp/validation";
|
|
137
|
+
|
|
138
|
+
const schema =
|
|
139
|
+
v.object({
|
|
140
|
+
email:
|
|
141
|
+
v.string({
|
|
142
|
+
email: true,
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export async function POST(
|
|
147
|
+
request: Request
|
|
148
|
+
) {
|
|
149
|
+
const input =
|
|
150
|
+
await request.json();
|
|
151
|
+
const result =
|
|
152
|
+
schema.safeParse(
|
|
153
|
+
input
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
return Response.json(
|
|
158
|
+
result,
|
|
159
|
+
{
|
|
160
|
+
status: 422,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Response.json({
|
|
166
|
+
user: result.data,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Throwing parse
|
|
172
|
+
|
|
173
|
+
Use `parse()` when invalid input should throw a `ValidationError`.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import {
|
|
177
|
+
ValidationError,
|
|
178
|
+
parse,
|
|
179
|
+
} from "bcp/validation";
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const user =
|
|
183
|
+
parse(
|
|
184
|
+
userSchema,
|
|
185
|
+
input
|
|
186
|
+
);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (
|
|
189
|
+
error instanceof
|
|
190
|
+
ValidationError
|
|
191
|
+
) {
|
|
192
|
+
console.log(
|
|
193
|
+
error.fieldErrors
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Available validators
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
v.string()
|
|
203
|
+
v.number()
|
|
204
|
+
v.boolean()
|
|
205
|
+
v.literal("active")
|
|
206
|
+
v.enum(["admin", "user"])
|
|
207
|
+
v.array(v.string())
|
|
208
|
+
v.object({ ... })
|
|
209
|
+
v.union([ ... ])
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Optional and nullable values:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
v.string().optional()
|
|
216
|
+
v.string().nullable()
|
|
217
|
+
|
|
218
|
+
v.optional(v.string())
|
|
219
|
+
v.nullable(v.string())
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## String validation
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
v.string({
|
|
226
|
+
trim: true,
|
|
227
|
+
minLength: 2,
|
|
228
|
+
maxLength: 100,
|
|
229
|
+
email: true,
|
|
230
|
+
pattern: /^[A-Z]/,
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Number validation
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
v.number({
|
|
238
|
+
coerce: true,
|
|
239
|
+
integer: true,
|
|
240
|
+
min: 1,
|
|
241
|
+
max: 100,
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`coerce: true` is useful for HTML forms because FormData values are strings.
|
|
246
|
+
|
|
247
|
+
## Boolean validation
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
v.boolean({
|
|
251
|
+
coerce: true,
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Boolean coercion recognizes common form values including `true`, `false`, `1`, `0`, `on`, `off`, `yes` and `no`.
|
|
256
|
+
|
|
257
|
+
## Custom rules
|
|
258
|
+
|
|
259
|
+
Use `refine()` for application-specific rules.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const password =
|
|
263
|
+
v.string({
|
|
264
|
+
minLength: 8,
|
|
265
|
+
}).refine(
|
|
266
|
+
(value) =>
|
|
267
|
+
/\d/.test(value),
|
|
268
|
+
"Password must contain a number.",
|
|
269
|
+
"password_number"
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Unknown object fields
|
|
274
|
+
|
|
275
|
+
Object validation strips fields that are not declared in the schema by default.
|
|
276
|
+
|
|
277
|
+
This is useful for API and form input because untrusted extra fields do not automatically pass into database writes.
|
|
278
|
+
|
|
279
|
+
To preserve unknown fields explicitly:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
v.object(
|
|
283
|
+
{
|
|
284
|
+
name: v.string(),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
allowUnknown: true,
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Validation paths and errors
|
|
293
|
+
|
|
294
|
+
Each issue contains:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
{
|
|
298
|
+
path: [
|
|
299
|
+
"profile",
|
|
300
|
+
"email",
|
|
301
|
+
],
|
|
302
|
+
message:
|
|
303
|
+
"Must be a valid email address.",
|
|
304
|
+
code:
|
|
305
|
+
"invalid_email",
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Use `getFieldError()` when only the first message for a field is needed.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import {
|
|
313
|
+
getFieldError,
|
|
314
|
+
} from "bcp/validation";
|
|
315
|
+
|
|
316
|
+
const emailError =
|
|
317
|
+
getFieldError(
|
|
318
|
+
result,
|
|
319
|
+
"email"
|
|
320
|
+
);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Type inference
|
|
324
|
+
|
|
325
|
+
Use `InferValidator` when application code needs the TypeScript output type of a schema.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import type {
|
|
329
|
+
InferValidator,
|
|
330
|
+
} from "bcp/validation";
|
|
331
|
+
|
|
332
|
+
type UserInput =
|
|
333
|
+
InferValidator<
|
|
334
|
+
typeof userSchema
|
|
335
|
+
>;
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## External schema libraries
|
|
339
|
+
|
|
340
|
+
BCP 0.1.19 does not require Zod, Valibot or another validation dependency. The built-in API keeps framework validation dependency-free.
|
|
341
|
+
|
|
342
|
+
Adapters for external schema libraries can be added in future releases without changing the validation result model used by actions and API routes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"types": "./packages/client/src/config.ts",
|
|
44
44
|
"default": "./packages/client/src/config.ts"
|
|
45
45
|
},
|
|
46
|
+
"./validation": {
|
|
47
|
+
"types": "./packages/client/src/validation.ts",
|
|
48
|
+
"default": "./packages/client/src/validation.ts"
|
|
49
|
+
},
|
|
46
50
|
"./database": {
|
|
47
51
|
"types": "./packages/client/src/database.ts",
|
|
48
52
|
"browser": "./packages/client/src/server-only.browser.mjs",
|