ngx-api-forms 1.0.0
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/LICENSE +21 -0
- package/README.md +657 -0
- package/analog/index.d.ts +76 -0
- package/django/index.d.ts +56 -0
- package/express-validator/index.d.ts +60 -0
- package/fesm2022/ngx-api-forms-analog.mjs +195 -0
- package/fesm2022/ngx-api-forms-analog.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-django.mjs +173 -0
- package/fesm2022/ngx-api-forms-django.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-express-validator.mjs +202 -0
- package/fesm2022/ngx-api-forms-express-validator.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-laravel.mjs +167 -0
- package/fesm2022/ngx-api-forms-laravel.mjs.map +1 -0
- package/fesm2022/ngx-api-forms-zod.mjs +226 -0
- package/fesm2022/ngx-api-forms-zod.mjs.map +1 -0
- package/fesm2022/ngx-api-forms.mjs +890 -0
- package/fesm2022/ngx-api-forms.mjs.map +1 -0
- package/index.d.ts +621 -0
- package/laravel/index.d.ts +49 -0
- package/package.json +85 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +300 -0
- package/schematics/ng-add/index.spec.js +119 -0
- package/schematics/ng-add/schema.json +34 -0
- package/zod/index.d.ts +58 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ErrorPreset, ConstraintMap } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analog / Nitro / h3 error preset.
|
|
5
|
+
*
|
|
6
|
+
* Analog uses Nitro (powered by h3) for API routes. Validation errors thrown
|
|
7
|
+
* via `createError()` are wrapped in a Nitro envelope:
|
|
8
|
+
*
|
|
9
|
+
* ```json
|
|
10
|
+
* {
|
|
11
|
+
* "statusCode": 422,
|
|
12
|
+
* "statusMessage": "Validation failed",
|
|
13
|
+
* "data": {
|
|
14
|
+
* "email": ["This field is required."],
|
|
15
|
+
* "name": ["Must be at least 3 characters."]
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* This preset unwraps the `data` field and parses it as a flat
|
|
21
|
+
* `{ field: string[] }` structure (Django-like). It also supports:
|
|
22
|
+
*
|
|
23
|
+
* - Structured error codes: `{ "email": [{ "message": "...", "code": "required" }] }`
|
|
24
|
+
* - `non_field_errors` / `_errors` routed to `globalErrorsSignal`
|
|
25
|
+
* - `statusMessage` used as a global error when `data` is absent
|
|
26
|
+
* - Direct `{ field: string[] }` format (without Nitro envelope)
|
|
27
|
+
*
|
|
28
|
+
* For Zod validation in Analog routes, prefer `zodPreset` instead.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // server/routes/api/register.post.ts
|
|
33
|
+
* import { defineEventHandler, readBody, createError } from 'h3';
|
|
34
|
+
*
|
|
35
|
+
* export default defineEventHandler(async (event) => {
|
|
36
|
+
* const body = await readBody(event);
|
|
37
|
+
* const errors: Record<string, string[]> = {};
|
|
38
|
+
*
|
|
39
|
+
* if (!body.email) errors.email = ['This field is required.'];
|
|
40
|
+
* if (body.password?.length < 8) errors.password = ['Must be at least 8 characters.'];
|
|
41
|
+
*
|
|
42
|
+
* if (Object.keys(errors).length > 0) {
|
|
43
|
+
* throw createError({ statusCode: 422, data: errors });
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* return { id: 1, email: body.email };
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default constraint map for Analog / h3.
|
|
53
|
+
*/
|
|
54
|
+
declare const ANALOG_CONSTRAINT_MAP: ConstraintMap;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a preset for Analog / Nitro / h3 API routes.
|
|
57
|
+
*
|
|
58
|
+
* Unwraps the h3 `createError({ data })` envelope automatically.
|
|
59
|
+
* Supports both plain string arrays and structured `{ message, code }` objects.
|
|
60
|
+
*
|
|
61
|
+
* @param options.noInference - Skip English-language constraint guessing.
|
|
62
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { analogPreset } from 'ngx-api-forms/analog';
|
|
67
|
+
*
|
|
68
|
+
* const bridge = provideFormBridge(form, { preset: analogPreset() });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function analogPreset(options?: {
|
|
72
|
+
noInference?: boolean;
|
|
73
|
+
constraintPatterns?: Record<string, RegExp>;
|
|
74
|
+
}): ErrorPreset;
|
|
75
|
+
|
|
76
|
+
export { ANALOG_CONSTRAINT_MAP, analogPreset };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ErrorPreset, ConstraintMap } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Django REST Framework validation error preset.
|
|
5
|
+
*
|
|
6
|
+
* Parses the standard DRF validation error format:
|
|
7
|
+
* ```json
|
|
8
|
+
* {
|
|
9
|
+
* "email": ["This field is required."],
|
|
10
|
+
* "name": ["Ensure this field has at least 3 characters."]
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Also handles non-field errors:
|
|
15
|
+
* ```json
|
|
16
|
+
* {
|
|
17
|
+
* "non_field_errors": ["Unable to log in with provided credentials."],
|
|
18
|
+
* "email": ["Enter a valid email address."]
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default constraint map for Django REST Framework.
|
|
25
|
+
*/
|
|
26
|
+
declare const DJANGO_CONSTRAINT_MAP: ConstraintMap;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a Django REST Framework validation error preset.
|
|
29
|
+
*
|
|
30
|
+
* @param options.camelCase - If true, converts snake_case field names to camelCase (default: true)
|
|
31
|
+
* @param options.noInference - When true, skips English-language constraint guessing.
|
|
32
|
+
* The raw error message is used directly and the constraint is set to `'serverError'`.
|
|
33
|
+
* Use this when your backend returns translated or custom messages.
|
|
34
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
35
|
+
* Keys are constraint names, values are RegExp tested against the raw message.
|
|
36
|
+
* Checked before the built-in English patterns.
|
|
37
|
+
* Example: `{ required: /ce champ est obligatoire/i, email: /adresse.*invalide/i }`
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { djangoPreset } from 'ngx-api-forms/django';
|
|
42
|
+
*
|
|
43
|
+
* // Default: infer constraint from English messages
|
|
44
|
+
* const bridge = createFormBridge(form, { preset: djangoPreset() });
|
|
45
|
+
*
|
|
46
|
+
* // No inference: raw messages, no guessing
|
|
47
|
+
* const bridge = createFormBridge(form, { preset: djangoPreset({ noInference: true }) });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function djangoPreset(options?: {
|
|
51
|
+
camelCase?: boolean;
|
|
52
|
+
noInference?: boolean;
|
|
53
|
+
constraintPatterns?: Record<string, RegExp>;
|
|
54
|
+
}): ErrorPreset;
|
|
55
|
+
|
|
56
|
+
export { DJANGO_CONSTRAINT_MAP, djangoPreset };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ErrorPreset, ConstraintMap } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* express-validator error preset.
|
|
5
|
+
*
|
|
6
|
+
* Parses the standard express-validator error format (v7+):
|
|
7
|
+
* ```json
|
|
8
|
+
* {
|
|
9
|
+
* "errors": [
|
|
10
|
+
* { "type": "field", "value": "", "msg": "Invalid value", "path": "email", "location": "body" }
|
|
11
|
+
* ]
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Also handles the legacy v5/v6 format:
|
|
16
|
+
* ```json
|
|
17
|
+
* {
|
|
18
|
+
* "errors": [
|
|
19
|
+
* { "param": "email", "msg": "Invalid value", "location": "body" }
|
|
20
|
+
* ]
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* And the alternative grouped format (from `validationResult().mapped()` or `.array()`):
|
|
25
|
+
* ```json
|
|
26
|
+
* [
|
|
27
|
+
* { "type": "field", "path": "email", "msg": "Invalid email" }
|
|
28
|
+
* ]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default constraint map for express-validator.
|
|
34
|
+
*/
|
|
35
|
+
declare const EXPRESS_VALIDATOR_CONSTRAINT_MAP: ConstraintMap;
|
|
36
|
+
/**
|
|
37
|
+
* Creates an express-validator error preset.
|
|
38
|
+
*
|
|
39
|
+
* @param options.noInference - When true, skips English-language constraint guessing.
|
|
40
|
+
* All errors use `constraint: 'serverError'` with the original message preserved.
|
|
41
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
42
|
+
* Keys are constraint names, values are RegExp tested against the raw message.
|
|
43
|
+
* Checked before the built-in English patterns.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { expressValidatorPreset } from 'ngx-api-forms/express-validator';
|
|
48
|
+
*
|
|
49
|
+
* const bridge = createFormBridge(form, { preset: expressValidatorPreset() });
|
|
50
|
+
*
|
|
51
|
+
* // No inference: raw messages, no guessing
|
|
52
|
+
* const bridge = createFormBridge(form, { preset: expressValidatorPreset({ noInference: true }) });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function expressValidatorPreset(options?: {
|
|
56
|
+
noInference?: boolean;
|
|
57
|
+
constraintPatterns?: Record<string, RegExp>;
|
|
58
|
+
}): ErrorPreset;
|
|
59
|
+
|
|
60
|
+
export { EXPRESS_VALIDATOR_CONSTRAINT_MAP, expressValidatorPreset };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { GLOBAL_ERROR_FIELD } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analog / Nitro / h3 error preset.
|
|
5
|
+
*
|
|
6
|
+
* Analog uses Nitro (powered by h3) for API routes. Validation errors thrown
|
|
7
|
+
* via `createError()` are wrapped in a Nitro envelope:
|
|
8
|
+
*
|
|
9
|
+
* ```json
|
|
10
|
+
* {
|
|
11
|
+
* "statusCode": 422,
|
|
12
|
+
* "statusMessage": "Validation failed",
|
|
13
|
+
* "data": {
|
|
14
|
+
* "email": ["This field is required."],
|
|
15
|
+
* "name": ["Must be at least 3 characters."]
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* This preset unwraps the `data` field and parses it as a flat
|
|
21
|
+
* `{ field: string[] }` structure (Django-like). It also supports:
|
|
22
|
+
*
|
|
23
|
+
* - Structured error codes: `{ "email": [{ "message": "...", "code": "required" }] }`
|
|
24
|
+
* - `non_field_errors` / `_errors` routed to `globalErrorsSignal`
|
|
25
|
+
* - `statusMessage` used as a global error when `data` is absent
|
|
26
|
+
* - Direct `{ field: string[] }` format (without Nitro envelope)
|
|
27
|
+
*
|
|
28
|
+
* For Zod validation in Analog routes, prefer `zodPreset` instead.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // server/routes/api/register.post.ts
|
|
33
|
+
* import { defineEventHandler, readBody, createError } from 'h3';
|
|
34
|
+
*
|
|
35
|
+
* export default defineEventHandler(async (event) => {
|
|
36
|
+
* const body = await readBody(event);
|
|
37
|
+
* const errors: Record<string, string[]> = {};
|
|
38
|
+
*
|
|
39
|
+
* if (!body.email) errors.email = ['This field is required.'];
|
|
40
|
+
* if (body.password?.length < 8) errors.password = ['Must be at least 8 characters.'];
|
|
41
|
+
*
|
|
42
|
+
* if (Object.keys(errors).length > 0) {
|
|
43
|
+
* throw createError({ statusCode: 422, data: errors });
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* return { id: 1, email: body.email };
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Tries user-provided constraint patterns against a message.
|
|
52
|
+
* Returns the matched constraint key or null.
|
|
53
|
+
*/
|
|
54
|
+
function matchUserPatterns(message, patterns) {
|
|
55
|
+
for (const [constraint, regex] of Object.entries(patterns)) {
|
|
56
|
+
if (regex.test(message))
|
|
57
|
+
return constraint;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Infers a constraint key from a validation message.
|
|
63
|
+
*/
|
|
64
|
+
function inferConstraint(message) {
|
|
65
|
+
const lower = message.toLowerCase();
|
|
66
|
+
if (lower.includes('required') || lower.includes('may not be blank'))
|
|
67
|
+
return 'required';
|
|
68
|
+
if (lower.includes('valid email') || lower.includes('must be an email'))
|
|
69
|
+
return 'email';
|
|
70
|
+
if (lower.includes('at least') && lower.includes('character'))
|
|
71
|
+
return 'minlength';
|
|
72
|
+
if (lower.includes('at most') && lower.includes('character'))
|
|
73
|
+
return 'maxlength';
|
|
74
|
+
if (lower.includes('at least') || lower.includes('greater than or equal') || lower.includes('minimum'))
|
|
75
|
+
return 'min';
|
|
76
|
+
if (lower.includes('at most') || lower.includes('less than or equal') || lower.includes('maximum'))
|
|
77
|
+
return 'max';
|
|
78
|
+
if (lower.includes('must be a number') || lower.includes('numeric'))
|
|
79
|
+
return 'number';
|
|
80
|
+
if (lower.includes('must be an integer'))
|
|
81
|
+
return 'integer';
|
|
82
|
+
if (lower.includes('valid date'))
|
|
83
|
+
return 'date';
|
|
84
|
+
if (lower.includes('valid url'))
|
|
85
|
+
return 'url';
|
|
86
|
+
if (lower.includes('already exists') || lower.includes('already taken'))
|
|
87
|
+
return 'unique';
|
|
88
|
+
if (lower.includes('must match') || lower.includes('does not match'))
|
|
89
|
+
return 'pattern';
|
|
90
|
+
return 'serverError';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Default constraint map for Analog / h3.
|
|
94
|
+
*/
|
|
95
|
+
const ANALOG_CONSTRAINT_MAP = {
|
|
96
|
+
required: 'required',
|
|
97
|
+
email: 'email',
|
|
98
|
+
minlength: 'minlength',
|
|
99
|
+
maxlength: 'maxlength',
|
|
100
|
+
min: 'min',
|
|
101
|
+
max: 'max',
|
|
102
|
+
number: 'number',
|
|
103
|
+
integer: 'integer',
|
|
104
|
+
date: 'date',
|
|
105
|
+
url: 'url',
|
|
106
|
+
unique: 'unique',
|
|
107
|
+
pattern: 'pattern',
|
|
108
|
+
invalid: 'invalid',
|
|
109
|
+
serverError: 'serverError',
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Creates a preset for Analog / Nitro / h3 API routes.
|
|
113
|
+
*
|
|
114
|
+
* Unwraps the h3 `createError({ data })` envelope automatically.
|
|
115
|
+
* Supports both plain string arrays and structured `{ message, code }` objects.
|
|
116
|
+
*
|
|
117
|
+
* @param options.noInference - Skip English-language constraint guessing.
|
|
118
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* import { analogPreset } from 'ngx-api-forms/analog';
|
|
123
|
+
*
|
|
124
|
+
* const bridge = provideFormBridge(form, { preset: analogPreset() });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
function analogPreset(options) {
|
|
128
|
+
const skipInference = options?.noInference ?? false;
|
|
129
|
+
const userPatterns = options?.constraintPatterns;
|
|
130
|
+
return {
|
|
131
|
+
name: 'analog',
|
|
132
|
+
constraintMap: ANALOG_CONSTRAINT_MAP,
|
|
133
|
+
parse(error) {
|
|
134
|
+
if (!error || typeof error !== 'object' || Array.isArray(error))
|
|
135
|
+
return [];
|
|
136
|
+
const err = error;
|
|
137
|
+
// Unwrap Nitro/h3 envelope: { statusCode, statusMessage, data }
|
|
138
|
+
let fieldData;
|
|
139
|
+
if (err['data'] && typeof err['data'] === 'object' && !Array.isArray(err['data'])) {
|
|
140
|
+
fieldData = err['data'];
|
|
141
|
+
}
|
|
142
|
+
// Direct format (no envelope): { field: [messages] }
|
|
143
|
+
else if (!err['statusCode'] && !err['message'] && !err['errors']) {
|
|
144
|
+
fieldData = err;
|
|
145
|
+
}
|
|
146
|
+
// Nitro envelope without structured data: { statusCode, statusMessage }
|
|
147
|
+
else if (typeof err['statusMessage'] === 'string' && !err['data']) {
|
|
148
|
+
return [{
|
|
149
|
+
field: GLOBAL_ERROR_FIELD,
|
|
150
|
+
constraint: 'serverError',
|
|
151
|
+
message: err['statusMessage'],
|
|
152
|
+
}];
|
|
153
|
+
}
|
|
154
|
+
if (!fieldData)
|
|
155
|
+
return [];
|
|
156
|
+
const result = [];
|
|
157
|
+
for (const [rawField, messages] of Object.entries(fieldData)) {
|
|
158
|
+
if (!Array.isArray(messages))
|
|
159
|
+
continue;
|
|
160
|
+
const isGlobal = rawField === 'non_field_errors' || rawField === '_errors';
|
|
161
|
+
const field = isGlobal ? GLOBAL_ERROR_FIELD : rawField;
|
|
162
|
+
for (const item of messages) {
|
|
163
|
+
// Structured format: { message: string, code: string }
|
|
164
|
+
if (typeof item === 'object' && item !== null && 'message' in item && 'code' in item) {
|
|
165
|
+
const structured = item;
|
|
166
|
+
result.push({ field, constraint: structured.code, message: structured.message });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Standard format: plain string
|
|
170
|
+
if (typeof item !== 'string')
|
|
171
|
+
continue;
|
|
172
|
+
let constraint;
|
|
173
|
+
if (skipInference) {
|
|
174
|
+
constraint = 'serverError';
|
|
175
|
+
}
|
|
176
|
+
else if (userPatterns) {
|
|
177
|
+
constraint = matchUserPatterns(item, userPatterns) ?? inferConstraint(item);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
constraint = inferConstraint(item);
|
|
181
|
+
}
|
|
182
|
+
result.push({ field, constraint, message: item });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generated bundle index. Do not edit.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
export { ANALOG_CONSTRAINT_MAP, analogPreset };
|
|
195
|
+
//# sourceMappingURL=ngx-api-forms-analog.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-api-forms-analog.mjs","sources":["../../../projects/ngx-api-forms/analog/src/analog.preset.ts","../../../projects/ngx-api-forms/analog/src/ngx-api-forms-analog.ts"],"sourcesContent":["/**\n * Analog / Nitro / h3 error preset.\n *\n * Analog uses Nitro (powered by h3) for API routes. Validation errors thrown\n * via `createError()` are wrapped in a Nitro envelope:\n *\n * ```json\n * {\n * \"statusCode\": 422,\n * \"statusMessage\": \"Validation failed\",\n * \"data\": {\n * \"email\": [\"This field is required.\"],\n * \"name\": [\"Must be at least 3 characters.\"]\n * }\n * }\n * ```\n *\n * This preset unwraps the `data` field and parses it as a flat\n * `{ field: string[] }` structure (Django-like). It also supports:\n *\n * - Structured error codes: `{ \"email\": [{ \"message\": \"...\", \"code\": \"required\" }] }`\n * - `non_field_errors` / `_errors` routed to `globalErrorsSignal`\n * - `statusMessage` used as a global error when `data` is absent\n * - Direct `{ field: string[] }` format (without Nitro envelope)\n *\n * For Zod validation in Analog routes, prefer `zodPreset` instead.\n *\n * @example\n * ```typescript\n * // server/routes/api/register.post.ts\n * import { defineEventHandler, readBody, createError } from 'h3';\n *\n * export default defineEventHandler(async (event) => {\n * const body = await readBody(event);\n * const errors: Record<string, string[]> = {};\n *\n * if (!body.email) errors.email = ['This field is required.'];\n * if (body.password?.length < 8) errors.password = ['Must be at least 8 characters.'];\n *\n * if (Object.keys(errors).length > 0) {\n * throw createError({ statusCode: 422, data: errors });\n * }\n *\n * return { id: 1, email: body.email };\n * });\n * ```\n */\nimport { ApiFieldError, ConstraintMap, ErrorPreset, GLOBAL_ERROR_FIELD } from 'ngx-api-forms';\n\n/** Shape of a structured error with a code (language-independent). */\ninterface StructuredError {\n message: string;\n code: string;\n}\n\n/**\n * Tries user-provided constraint patterns against a message.\n * Returns the matched constraint key or null.\n */\nfunction matchUserPatterns(message: string, patterns: Record<string, RegExp>): string | null {\n for (const [constraint, regex] of Object.entries(patterns)) {\n if (regex.test(message)) return constraint;\n }\n return null;\n}\n\n/**\n * Infers a constraint key from a validation message.\n */\nfunction inferConstraint(message: string): string {\n const lower = message.toLowerCase();\n\n if (lower.includes('required') || lower.includes('may not be blank')) return 'required';\n if (lower.includes('valid email') || lower.includes('must be an email')) return 'email';\n if (lower.includes('at least') && lower.includes('character')) return 'minlength';\n if (lower.includes('at most') && lower.includes('character')) return 'maxlength';\n if (lower.includes('at least') || lower.includes('greater than or equal') || lower.includes('minimum')) return 'min';\n if (lower.includes('at most') || lower.includes('less than or equal') || lower.includes('maximum')) return 'max';\n if (lower.includes('must be a number') || lower.includes('numeric')) return 'number';\n if (lower.includes('must be an integer')) return 'integer';\n if (lower.includes('valid date')) return 'date';\n if (lower.includes('valid url')) return 'url';\n if (lower.includes('already exists') || lower.includes('already taken')) return 'unique';\n if (lower.includes('must match') || lower.includes('does not match')) return 'pattern';\n\n return 'serverError';\n}\n\n/**\n * Default constraint map for Analog / h3.\n */\nexport const ANALOG_CONSTRAINT_MAP: ConstraintMap = {\n required: 'required',\n email: 'email',\n minlength: 'minlength',\n maxlength: 'maxlength',\n min: 'min',\n max: 'max',\n number: 'number',\n integer: 'integer',\n date: 'date',\n url: 'url',\n unique: 'unique',\n pattern: 'pattern',\n invalid: 'invalid',\n serverError: 'serverError',\n};\n\n/**\n * Creates a preset for Analog / Nitro / h3 API routes.\n *\n * Unwraps the h3 `createError({ data })` envelope automatically.\n * Supports both plain string arrays and structured `{ message, code }` objects.\n *\n * @param options.noInference - Skip English-language constraint guessing.\n * @param options.constraintPatterns - Custom regex patterns for constraint inference.\n *\n * @example\n * ```typescript\n * import { analogPreset } from 'ngx-api-forms/analog';\n *\n * const bridge = provideFormBridge(form, { preset: analogPreset() });\n * ```\n */\nexport function analogPreset(options?: { noInference?: boolean; constraintPatterns?: Record<string, RegExp> }): ErrorPreset {\n const skipInference = options?.noInference ?? false;\n const userPatterns = options?.constraintPatterns;\n\n return {\n name: 'analog',\n constraintMap: ANALOG_CONSTRAINT_MAP,\n parse(error: unknown): ApiFieldError[] {\n if (!error || typeof error !== 'object' || Array.isArray(error)) return [];\n\n const err = error as Record<string, unknown>;\n\n // Unwrap Nitro/h3 envelope: { statusCode, statusMessage, data }\n let fieldData: Record<string, unknown> | undefined;\n\n if (err['data'] && typeof err['data'] === 'object' && !Array.isArray(err['data'])) {\n fieldData = err['data'] as Record<string, unknown>;\n }\n // Direct format (no envelope): { field: [messages] }\n else if (!err['statusCode'] && !err['message'] && !err['errors']) {\n fieldData = err as Record<string, unknown>;\n }\n // Nitro envelope without structured data: { statusCode, statusMessage }\n else if (typeof err['statusMessage'] === 'string' && !err['data']) {\n return [{\n field: GLOBAL_ERROR_FIELD,\n constraint: 'serverError',\n message: err['statusMessage'] as string,\n }];\n }\n\n if (!fieldData) return [];\n\n const result: ApiFieldError[] = [];\n\n for (const [rawField, messages] of Object.entries(fieldData)) {\n if (!Array.isArray(messages)) continue;\n\n const isGlobal = rawField === 'non_field_errors' || rawField === '_errors';\n const field = isGlobal ? GLOBAL_ERROR_FIELD : rawField;\n\n for (const item of messages) {\n // Structured format: { message: string, code: string }\n if (typeof item === 'object' && item !== null && 'message' in item && 'code' in item) {\n const structured = item as StructuredError;\n result.push({ field, constraint: structured.code, message: structured.message });\n continue;\n }\n\n // Standard format: plain string\n if (typeof item !== 'string') continue;\n let constraint: string;\n if (skipInference) {\n constraint = 'serverError';\n } else if (userPatterns) {\n constraint = matchUserPatterns(item, userPatterns) ?? inferConstraint(item);\n } else {\n constraint = inferConstraint(item);\n }\n result.push({ field, constraint, message: item });\n }\n }\n\n return result;\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { GLOBAL_ERROR_FIELD } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Django REST Framework validation error preset.
|
|
5
|
+
*
|
|
6
|
+
* Parses the standard DRF validation error format:
|
|
7
|
+
* ```json
|
|
8
|
+
* {
|
|
9
|
+
* "email": ["This field is required."],
|
|
10
|
+
* "name": ["Ensure this field has at least 3 characters."]
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Also handles non-field errors:
|
|
15
|
+
* ```json
|
|
16
|
+
* {
|
|
17
|
+
* "non_field_errors": ["Unable to log in with provided credentials."],
|
|
18
|
+
* "email": ["Enter a valid email address."]
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Infers a constraint key from a Django REST Framework validation message.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* This function relies on English-language pattern matching (e.g. "this field is required",
|
|
27
|
+
* "valid email"). If your Django backend returns translated messages (USE_I18N=True with
|
|
28
|
+
* non-English locale), the inference will fall back to 'serverError'. In that case, use
|
|
29
|
+
* structured error codes (`{ message, code }`) or `constraintPatterns` for reliable i18n.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Tries user-provided constraint patterns against a message.
|
|
33
|
+
* Returns the matched constraint key or null.
|
|
34
|
+
*/
|
|
35
|
+
function matchUserPatterns(message, patterns) {
|
|
36
|
+
for (const [constraint, regex] of Object.entries(patterns)) {
|
|
37
|
+
if (regex.test(message))
|
|
38
|
+
return constraint;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function inferConstraint(message) {
|
|
43
|
+
const lower = message.toLowerCase();
|
|
44
|
+
if (lower.includes('this field is required') || lower.includes('this field may not be blank'))
|
|
45
|
+
return 'required';
|
|
46
|
+
if (lower.includes('valid email'))
|
|
47
|
+
return 'email';
|
|
48
|
+
if (lower.includes('at least') && lower.includes('character'))
|
|
49
|
+
return 'minlength';
|
|
50
|
+
if (lower.includes('no more than') && lower.includes('character'))
|
|
51
|
+
return 'maxlength';
|
|
52
|
+
if (lower.includes('ensure this value is greater than or equal'))
|
|
53
|
+
return 'min';
|
|
54
|
+
if (lower.includes('ensure this value is less than or equal'))
|
|
55
|
+
return 'max';
|
|
56
|
+
if (lower.includes('a valid integer'))
|
|
57
|
+
return 'integer';
|
|
58
|
+
if (lower.includes('a valid number'))
|
|
59
|
+
return 'number';
|
|
60
|
+
if (lower.includes('valid date'))
|
|
61
|
+
return 'date';
|
|
62
|
+
if (lower.includes('valid url'))
|
|
63
|
+
return 'url';
|
|
64
|
+
if (lower.includes('already exists'))
|
|
65
|
+
return 'unique';
|
|
66
|
+
if (lower.includes('valid phone'))
|
|
67
|
+
return 'phone';
|
|
68
|
+
if (lower.includes('does not match'))
|
|
69
|
+
return 'pattern';
|
|
70
|
+
return 'serverError';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Converts Django snake_case field names to camelCase.
|
|
74
|
+
*/
|
|
75
|
+
function snakeToCamel(str) {
|
|
76
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Default constraint map for Django REST Framework.
|
|
80
|
+
*/
|
|
81
|
+
const DJANGO_CONSTRAINT_MAP = {
|
|
82
|
+
required: 'required',
|
|
83
|
+
email: 'email',
|
|
84
|
+
minlength: 'minlength',
|
|
85
|
+
maxlength: 'maxlength',
|
|
86
|
+
min: 'min',
|
|
87
|
+
max: 'max',
|
|
88
|
+
integer: 'integer',
|
|
89
|
+
number: 'number',
|
|
90
|
+
date: 'date',
|
|
91
|
+
url: 'url',
|
|
92
|
+
unique: 'unique',
|
|
93
|
+
phone: 'phone',
|
|
94
|
+
pattern: 'pattern',
|
|
95
|
+
invalid: 'invalid',
|
|
96
|
+
serverError: 'serverError',
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Creates a Django REST Framework validation error preset.
|
|
100
|
+
*
|
|
101
|
+
* @param options.camelCase - If true, converts snake_case field names to camelCase (default: true)
|
|
102
|
+
* @param options.noInference - When true, skips English-language constraint guessing.
|
|
103
|
+
* The raw error message is used directly and the constraint is set to `'serverError'`.
|
|
104
|
+
* Use this when your backend returns translated or custom messages.
|
|
105
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
106
|
+
* Keys are constraint names, values are RegExp tested against the raw message.
|
|
107
|
+
* Checked before the built-in English patterns.
|
|
108
|
+
* Example: `{ required: /ce champ est obligatoire/i, email: /adresse.*invalide/i }`
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* import { djangoPreset } from 'ngx-api-forms/django';
|
|
113
|
+
*
|
|
114
|
+
* // Default: infer constraint from English messages
|
|
115
|
+
* const bridge = createFormBridge(form, { preset: djangoPreset() });
|
|
116
|
+
*
|
|
117
|
+
* // No inference: raw messages, no guessing
|
|
118
|
+
* const bridge = createFormBridge(form, { preset: djangoPreset({ noInference: true }) });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
function djangoPreset(options) {
|
|
122
|
+
const shouldCamelCase = options?.camelCase ?? true;
|
|
123
|
+
const skipInference = options?.noInference ?? false;
|
|
124
|
+
const userPatterns = options?.constraintPatterns;
|
|
125
|
+
return {
|
|
126
|
+
name: 'django',
|
|
127
|
+
constraintMap: DJANGO_CONSTRAINT_MAP,
|
|
128
|
+
parse(error) {
|
|
129
|
+
if (!error || typeof error !== 'object' || Array.isArray(error))
|
|
130
|
+
return [];
|
|
131
|
+
const errors = error;
|
|
132
|
+
const result = [];
|
|
133
|
+
for (const [rawField, messages] of Object.entries(errors)) {
|
|
134
|
+
if (!Array.isArray(messages))
|
|
135
|
+
continue;
|
|
136
|
+
const isGlobal = rawField === 'non_field_errors' || rawField === 'detail';
|
|
137
|
+
const field = isGlobal
|
|
138
|
+
? GLOBAL_ERROR_FIELD
|
|
139
|
+
: (shouldCamelCase ? snakeToCamel(rawField) : rawField);
|
|
140
|
+
for (const item of messages) {
|
|
141
|
+
// Structured format: { message: string, code: string }
|
|
142
|
+
if (typeof item === 'object' && item !== null && 'message' in item && 'code' in item) {
|
|
143
|
+
const structured = item;
|
|
144
|
+
result.push({ field, constraint: structured.code, message: structured.message });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Standard format: plain string
|
|
148
|
+
if (typeof item !== 'string')
|
|
149
|
+
continue;
|
|
150
|
+
let constraint;
|
|
151
|
+
if (skipInference) {
|
|
152
|
+
constraint = 'serverError';
|
|
153
|
+
}
|
|
154
|
+
else if (userPatterns) {
|
|
155
|
+
constraint = matchUserPatterns(item, userPatterns) ?? inferConstraint(item);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
constraint = inferConstraint(item);
|
|
159
|
+
}
|
|
160
|
+
result.push({ field, constraint, message: item });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generated bundle index. Do not edit.
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
export { DJANGO_CONSTRAINT_MAP, djangoPreset };
|
|
173
|
+
//# sourceMappingURL=ngx-api-forms-django.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-api-forms-django.mjs","sources":["../../../projects/ngx-api-forms/django/src/django.preset.ts","../../../projects/ngx-api-forms/django/src/ngx-api-forms-django.ts"],"sourcesContent":["/**\n * Django REST Framework validation error preset.\n *\n * Parses the standard DRF validation error format:\n * ```json\n * {\n * \"email\": [\"This field is required.\"],\n * \"name\": [\"Ensure this field has at least 3 characters.\"]\n * }\n * ```\n *\n * Also handles non-field errors:\n * ```json\n * {\n * \"non_field_errors\": [\"Unable to log in with provided credentials.\"],\n * \"email\": [\"Enter a valid email address.\"]\n * }\n * ```\n */\nimport { ApiFieldError, ConstraintMap, ErrorPreset, GLOBAL_ERROR_FIELD } from 'ngx-api-forms';\n\n/**\n * Shape of a structured DRF error object (when using a custom exception handler\n * that includes the validator `code`).\n */\ninterface DjangoStructuredError {\n message: string;\n code: string;\n}\n\n/**\n * Infers a constraint key from a Django REST Framework validation message.\n *\n * @remarks\n * This function relies on English-language pattern matching (e.g. \"this field is required\",\n * \"valid email\"). If your Django backend returns translated messages (USE_I18N=True with\n * non-English locale), the inference will fall back to 'serverError'. In that case, use\n * structured error codes (`{ message, code }`) or `constraintPatterns` for reliable i18n.\n */\n/**\n * Tries user-provided constraint patterns against a message.\n * Returns the matched constraint key or null.\n */\nfunction matchUserPatterns(message: string, patterns: Record<string, RegExp>): string | null {\n for (const [constraint, regex] of Object.entries(patterns)) {\n if (regex.test(message)) return constraint;\n }\n return null;\n}\n\nfunction inferConstraint(message: string): string {\n const lower = message.toLowerCase();\n\n if (lower.includes('this field is required') || lower.includes('this field may not be blank')) return 'required';\n if (lower.includes('valid email')) return 'email';\n if (lower.includes('at least') && lower.includes('character')) return 'minlength';\n if (lower.includes('no more than') && lower.includes('character')) return 'maxlength';\n if (lower.includes('ensure this value is greater than or equal')) return 'min';\n if (lower.includes('ensure this value is less than or equal')) return 'max';\n if (lower.includes('a valid integer')) return 'integer';\n if (lower.includes('a valid number')) return 'number';\n if (lower.includes('valid date')) return 'date';\n if (lower.includes('valid url')) return 'url';\n if (lower.includes('already exists')) return 'unique';\n if (lower.includes('valid phone')) return 'phone';\n if (lower.includes('does not match')) return 'pattern';\n\n return 'serverError';\n}\n\n/**\n * Converts Django snake_case field names to camelCase.\n */\nfunction snakeToCamel(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());\n}\n\n/**\n * Default constraint map for Django REST Framework.\n */\nexport const DJANGO_CONSTRAINT_MAP: ConstraintMap = {\n required: 'required',\n email: 'email',\n minlength: 'minlength',\n maxlength: 'maxlength',\n min: 'min',\n max: 'max',\n integer: 'integer',\n number: 'number',\n date: 'date',\n url: 'url',\n unique: 'unique',\n phone: 'phone',\n pattern: 'pattern',\n invalid: 'invalid',\n serverError: 'serverError',\n};\n\n/**\n * Creates a Django REST Framework validation error preset.\n *\n * @param options.camelCase - If true, converts snake_case field names to camelCase (default: true)\n * @param options.noInference - When true, skips English-language constraint guessing.\n * The raw error message is used directly and the constraint is set to `'serverError'`.\n * Use this when your backend returns translated or custom messages.\n * @param options.constraintPatterns - Custom regex patterns for constraint inference.\n * Keys are constraint names, values are RegExp tested against the raw message.\n * Checked before the built-in English patterns.\n * Example: `{ required: /ce champ est obligatoire/i, email: /adresse.*invalide/i }`\n *\n * @example\n * ```typescript\n * import { djangoPreset } from 'ngx-api-forms/django';\n *\n * // Default: infer constraint from English messages\n * const bridge = createFormBridge(form, { preset: djangoPreset() });\n *\n * // No inference: raw messages, no guessing\n * const bridge = createFormBridge(form, { preset: djangoPreset({ noInference: true }) });\n * ```\n */\nexport function djangoPreset(options?: { camelCase?: boolean; noInference?: boolean; constraintPatterns?: Record<string, RegExp> }): ErrorPreset {\n const shouldCamelCase = options?.camelCase ?? true;\n const skipInference = options?.noInference ?? false;\n const userPatterns = options?.constraintPatterns;\n\n return {\n name: 'django',\n constraintMap: DJANGO_CONSTRAINT_MAP,\n parse(error: unknown): ApiFieldError[] {\n if (!error || typeof error !== 'object' || Array.isArray(error)) return [];\n\n const errors = error as Record<string, unknown>;\n const result: ApiFieldError[] = [];\n\n for (const [rawField, messages] of Object.entries(errors)) {\n if (!Array.isArray(messages)) continue;\n\n const isGlobal = rawField === 'non_field_errors' || rawField === 'detail';\n const field = isGlobal\n ? GLOBAL_ERROR_FIELD\n : (shouldCamelCase ? snakeToCamel(rawField) : rawField);\n\n for (const item of messages) {\n // Structured format: { message: string, code: string }\n if (typeof item === 'object' && item !== null && 'message' in item && 'code' in item) {\n const structured = item as DjangoStructuredError;\n result.push({ field, constraint: structured.code, message: structured.message });\n continue;\n }\n\n // Standard format: plain string\n if (typeof item !== 'string') continue;\n let constraint: string;\n if (skipInference) {\n constraint = 'serverError';\n } else if (userPatterns) {\n constraint = matchUserPatterns(item, userPatterns) ?? inferConstraint(item);\n } else {\n constraint = inferConstraint(item);\n }\n result.push({ field, constraint, message: item });\n }\n }\n\n return result;\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|