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
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ngx-api-forms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bridge API validation errors to Angular Reactive Forms. Supports NestJS/class-validator, Zod, Laravel, Django REST, express-validator and custom presets.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"forms",
|
|
8
|
+
"api",
|
|
9
|
+
"validation",
|
|
10
|
+
"error-handling",
|
|
11
|
+
"nestjs",
|
|
12
|
+
"class-validator",
|
|
13
|
+
"reactive-forms",
|
|
14
|
+
"form-helper",
|
|
15
|
+
"ng",
|
|
16
|
+
"ngx",
|
|
17
|
+
"i18n",
|
|
18
|
+
"ssr",
|
|
19
|
+
"zod",
|
|
20
|
+
"laravel",
|
|
21
|
+
"django",
|
|
22
|
+
"express",
|
|
23
|
+
"express-validator",
|
|
24
|
+
"angular-validation-errors",
|
|
25
|
+
"422",
|
|
26
|
+
"server-side-validation",
|
|
27
|
+
"form-errors",
|
|
28
|
+
"http-interceptor"
|
|
29
|
+
],
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Mikhaël GERBET",
|
|
32
|
+
"email": "frollon.noir@gmail.com",
|
|
33
|
+
"url": "https://github.com/MikhaelGerbet"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/MikhaelGerbet/ngx-api-forms.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/MikhaelGerbet/ngx-api-forms/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/MikhaelGerbet/ngx-api-forms#readme",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
46
|
+
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
47
|
+
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"tslib": "^2.3.0"
|
|
51
|
+
},
|
|
52
|
+
"schematics": "./schematics/collection.json",
|
|
53
|
+
"sideEffects": false,
|
|
54
|
+
"module": "fesm2022/ngx-api-forms.mjs",
|
|
55
|
+
"typings": "index.d.ts",
|
|
56
|
+
"exports": {
|
|
57
|
+
"./package.json": {
|
|
58
|
+
"default": "./package.json"
|
|
59
|
+
},
|
|
60
|
+
".": {
|
|
61
|
+
"types": "./index.d.ts",
|
|
62
|
+
"default": "./fesm2022/ngx-api-forms.mjs"
|
|
63
|
+
},
|
|
64
|
+
"./analog": {
|
|
65
|
+
"types": "./analog/index.d.ts",
|
|
66
|
+
"default": "./fesm2022/ngx-api-forms-analog.mjs"
|
|
67
|
+
},
|
|
68
|
+
"./django": {
|
|
69
|
+
"types": "./django/index.d.ts",
|
|
70
|
+
"default": "./fesm2022/ngx-api-forms-django.mjs"
|
|
71
|
+
},
|
|
72
|
+
"./express-validator": {
|
|
73
|
+
"types": "./express-validator/index.d.ts",
|
|
74
|
+
"default": "./fesm2022/ngx-api-forms-express-validator.mjs"
|
|
75
|
+
},
|
|
76
|
+
"./laravel": {
|
|
77
|
+
"types": "./laravel/index.d.ts",
|
|
78
|
+
"default": "./fesm2022/ngx-api-forms-laravel.mjs"
|
|
79
|
+
},
|
|
80
|
+
"./zod": {
|
|
81
|
+
"types": "./zod/index.d.ts",
|
|
82
|
+
"default": "./fesm2022/ngx-api-forms-zod.mjs"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const schematics = require("@angular-devkit/schematics");
|
|
4
|
+
const tasks = require("@angular-devkit/schematics/tasks");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps preset names to their import configuration.
|
|
8
|
+
* class-validator lives in the primary entry point; all others are secondary.
|
|
9
|
+
*/
|
|
10
|
+
const PRESET_CONFIG = {
|
|
11
|
+
"class-validator": {
|
|
12
|
+
importName: "classValidatorPreset",
|
|
13
|
+
importPath: "ngx-api-forms",
|
|
14
|
+
call: "classValidatorPreset()",
|
|
15
|
+
},
|
|
16
|
+
laravel: {
|
|
17
|
+
importName: "laravelPreset",
|
|
18
|
+
importPath: "ngx-api-forms/laravel",
|
|
19
|
+
call: "laravelPreset()",
|
|
20
|
+
},
|
|
21
|
+
django: {
|
|
22
|
+
importName: "djangoPreset",
|
|
23
|
+
importPath: "ngx-api-forms/django",
|
|
24
|
+
call: "djangoPreset()",
|
|
25
|
+
},
|
|
26
|
+
zod: {
|
|
27
|
+
importName: "zodPreset",
|
|
28
|
+
importPath: "ngx-api-forms/zod",
|
|
29
|
+
call: "zodPreset()",
|
|
30
|
+
},
|
|
31
|
+
"express-validator": {
|
|
32
|
+
importName: "expressValidatorPreset",
|
|
33
|
+
importPath: "ngx-api-forms/express-validator",
|
|
34
|
+
call: "expressValidatorPreset()",
|
|
35
|
+
},
|
|
36
|
+
analog: {
|
|
37
|
+
importName: "analogPreset",
|
|
38
|
+
importPath: "ngx-api-forms/analog",
|
|
39
|
+
call: "analogPreset()",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function ngAdd(options) {
|
|
44
|
+
return (tree, context) => {
|
|
45
|
+
context.addTask(new tasks.NodePackageInstallTask());
|
|
46
|
+
|
|
47
|
+
const preset = options.preset || "class-validator";
|
|
48
|
+
const config = PRESET_CONFIG[preset] || PRESET_CONFIG["class-validator"];
|
|
49
|
+
|
|
50
|
+
// 1. Create example component
|
|
51
|
+
const examplePath = "src/app/api-forms-example.component.ts";
|
|
52
|
+
if (!tree.exists(examplePath)) {
|
|
53
|
+
tree.create(examplePath, buildExampleContent(config));
|
|
54
|
+
context.logger.info(
|
|
55
|
+
"Created " + examplePath + " with " + preset + " preset."
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
context.logger.warn(
|
|
59
|
+
examplePath + " already exists. Skipping example generation."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Auto-inject interceptor into app.config.ts
|
|
64
|
+
const appConfigPath = "src/app/app.config.ts";
|
|
65
|
+
if (tree.exists(appConfigPath)) {
|
|
66
|
+
const content = tree.read(appConfigPath).toString("utf-8");
|
|
67
|
+
const updated = injectInterceptor(content);
|
|
68
|
+
if (updated !== content) {
|
|
69
|
+
tree.overwrite(appConfigPath, updated);
|
|
70
|
+
context.logger.info(
|
|
71
|
+
"Updated " + appConfigPath + ": added apiErrorInterceptor to HttpClient interceptors."
|
|
72
|
+
);
|
|
73
|
+
} else if (content.includes("apiErrorInterceptor")) {
|
|
74
|
+
context.logger.info(
|
|
75
|
+
appConfigPath + " already contains apiErrorInterceptor. Skipping."
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
context.logger.warn(
|
|
79
|
+
"Could not auto-inject interceptor into " + appConfigPath + ".\n" +
|
|
80
|
+
"Add it manually:\n\n" +
|
|
81
|
+
" import { provideHttpClient, withInterceptors } from '@angular/common/http';\n" +
|
|
82
|
+
" import { apiErrorInterceptor } from 'ngx-api-forms';\n\n" +
|
|
83
|
+
" provideHttpClient(withInterceptors([apiErrorInterceptor()]))\n"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
context.logger.warn(
|
|
88
|
+
"Could not find " + appConfigPath + ". Add the interceptor manually:\n\n" +
|
|
89
|
+
" import { provideHttpClient, withInterceptors } from '@angular/common/http';\n" +
|
|
90
|
+
" import { apiErrorInterceptor } from 'ngx-api-forms';\n\n" +
|
|
91
|
+
" provideHttpClient(withInterceptors([apiErrorInterceptor()]))\n"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return tree;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Attempts to inject `apiErrorInterceptor()` into an app.config.ts file.
|
|
101
|
+
* Returns the modified content, or the original if injection was not possible.
|
|
102
|
+
*
|
|
103
|
+
* Handles three cases:
|
|
104
|
+
* 1. `withInterceptors([...])` exists -> add apiErrorInterceptor() to the array
|
|
105
|
+
* 2. `provideHttpClient(...)` exists without withInterceptors -> add withInterceptors
|
|
106
|
+
* 3. Neither exists -> add provideHttpClient with interceptor to providers array
|
|
107
|
+
*/
|
|
108
|
+
function injectInterceptor(content) {
|
|
109
|
+
// Already present
|
|
110
|
+
if (content.includes("apiErrorInterceptor")) return content;
|
|
111
|
+
|
|
112
|
+
let result = content;
|
|
113
|
+
|
|
114
|
+
// Ensure apiErrorInterceptor import exists
|
|
115
|
+
// Check if there's already an import from 'ngx-api-forms'
|
|
116
|
+
const ngxImportRegex = /import\s*\{([^}]+)\}\s*from\s*['"]ngx-api-forms['"]/;
|
|
117
|
+
const ngxImportMatch = result.match(ngxImportRegex);
|
|
118
|
+
if (ngxImportMatch) {
|
|
119
|
+
// Add to existing import (trim to avoid double spaces)
|
|
120
|
+
const existingImports = ngxImportMatch[1].trim();
|
|
121
|
+
result = result.replace(
|
|
122
|
+
ngxImportMatch[0],
|
|
123
|
+
"import { " + existingImports + ", apiErrorInterceptor } from 'ngx-api-forms'"
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
// Add new import after last import line
|
|
127
|
+
const lastImportIndex = result.lastIndexOf("import ");
|
|
128
|
+
if (lastImportIndex !== -1) {
|
|
129
|
+
const afterImport = result.substring(lastImportIndex);
|
|
130
|
+
const semiIndex = afterImport.indexOf(";");
|
|
131
|
+
if (semiIndex !== -1) {
|
|
132
|
+
const importEnd = lastImportIndex + semiIndex + 1;
|
|
133
|
+
result =
|
|
134
|
+
result.substring(0, importEnd) +
|
|
135
|
+
"\nimport { apiErrorInterceptor } from 'ngx-api-forms';" +
|
|
136
|
+
result.substring(importEnd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Case 1: withInterceptors([...]) exists -> add to array
|
|
142
|
+
const withInterceptorsRegex = /withInterceptors\s*\(\s*\[([^\]]*)\]/;
|
|
143
|
+
const withInterceptorsMatch = result.match(withInterceptorsRegex);
|
|
144
|
+
if (withInterceptorsMatch) {
|
|
145
|
+
const existingInterceptors = withInterceptorsMatch[1].trim();
|
|
146
|
+
const separator = existingInterceptors.length > 0 ? ", " : "";
|
|
147
|
+
result = result.replace(
|
|
148
|
+
withInterceptorsMatch[0],
|
|
149
|
+
"withInterceptors([" + existingInterceptors + separator + "apiErrorInterceptor()]"
|
|
150
|
+
);
|
|
151
|
+
result = ensureHttpImport(result, "withInterceptors");
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Case 2: provideHttpClient(...) exists without withInterceptors
|
|
156
|
+
// Use balanced-paren scanner to handle nested calls like withFetch()
|
|
157
|
+
const phcIndex = result.indexOf("provideHttpClient(");
|
|
158
|
+
if (phcIndex !== -1) {
|
|
159
|
+
const openParen = phcIndex + "provideHttpClient".length;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let closeParen = -1;
|
|
162
|
+
for (let i = openParen; i < result.length; i++) {
|
|
163
|
+
if (result[i] === "(") depth++;
|
|
164
|
+
else if (result[i] === ")") {
|
|
165
|
+
depth--;
|
|
166
|
+
if (depth === 0) { closeParen = i; break; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (closeParen !== -1) {
|
|
170
|
+
const existingArgs = result.substring(openParen + 1, closeParen).trim();
|
|
171
|
+
const separator = existingArgs.length > 0 ? ", " : "";
|
|
172
|
+
result =
|
|
173
|
+
result.substring(0, openParen + 1) +
|
|
174
|
+
existingArgs + separator + "withInterceptors([apiErrorInterceptor()])" +
|
|
175
|
+
result.substring(closeParen);
|
|
176
|
+
result = ensureHttpImport(result, "provideHttpClient");
|
|
177
|
+
result = ensureHttpImport(result, "withInterceptors");
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Case 3: No provideHttpClient -> add to providers array
|
|
183
|
+
const providersRegex = /providers\s*:\s*\[/;
|
|
184
|
+
const providersMatch = result.match(providersRegex);
|
|
185
|
+
if (providersMatch) {
|
|
186
|
+
result = result.replace(
|
|
187
|
+
providersMatch[0],
|
|
188
|
+
providersMatch[0] +
|
|
189
|
+
"\n provideHttpClient(withInterceptors([apiErrorInterceptor()])),"
|
|
190
|
+
);
|
|
191
|
+
result = ensureHttpImport(result, "provideHttpClient");
|
|
192
|
+
result = ensureHttpImport(result, "withInterceptors");
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Could not inject
|
|
197
|
+
return content;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Ensures a symbol is imported from '@angular/common/http'.
|
|
202
|
+
* Only checks import statements, not usage in code.
|
|
203
|
+
*/
|
|
204
|
+
function ensureHttpImport(content, symbol) {
|
|
205
|
+
const httpImportRegex = /import\s*\{([^}]+)\}\s*from\s*['"]@angular\/common\/http['"]/;
|
|
206
|
+
const match = content.match(httpImportRegex);
|
|
207
|
+
|
|
208
|
+
if (match) {
|
|
209
|
+
// Already imported?
|
|
210
|
+
if (match[1].includes(symbol)) return content;
|
|
211
|
+
// Add to existing import
|
|
212
|
+
return content.replace(
|
|
213
|
+
match[0],
|
|
214
|
+
"import { " + match[1].trim() + ", " + symbol + " } from '@angular/common/http'"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add new import after last import
|
|
219
|
+
const lastImportIndex = content.lastIndexOf("import ");
|
|
220
|
+
if (lastImportIndex !== -1) {
|
|
221
|
+
const afterImport = content.substring(lastImportIndex);
|
|
222
|
+
const semiIndex = afterImport.indexOf(";");
|
|
223
|
+
if (semiIndex !== -1) {
|
|
224
|
+
const importEnd = lastImportIndex + semiIndex + 1;
|
|
225
|
+
return (
|
|
226
|
+
content.substring(0, importEnd) +
|
|
227
|
+
"\nimport { " + symbol + " } from '@angular/common/http';" +
|
|
228
|
+
content.substring(importEnd)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return content;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildExampleContent(config) {
|
|
236
|
+
const presetImportLine =
|
|
237
|
+
config.importPath === "ngx-api-forms"
|
|
238
|
+
? "import {\n" +
|
|
239
|
+
" provideFormBridge,\n" +
|
|
240
|
+
" " + config.importName + ",\n" +
|
|
241
|
+
" NgxFormErrorDirective,\n" +
|
|
242
|
+
"} from 'ngx-api-forms';\n"
|
|
243
|
+
: "import { provideFormBridge, NgxFormErrorDirective } from 'ngx-api-forms';\n" +
|
|
244
|
+
"import { " + config.importName + " } from '" + config.importPath + "';\n";
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
"import { Component, inject } from '@angular/core';\n" +
|
|
248
|
+
"import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';\n" +
|
|
249
|
+
"import { HttpClient } from '@angular/common/http';\n" +
|
|
250
|
+
presetImportLine +
|
|
251
|
+
"\n" +
|
|
252
|
+
"@Component({\n" +
|
|
253
|
+
" selector: 'app-api-forms-example',\n" +
|
|
254
|
+
" standalone: true,\n" +
|
|
255
|
+
" imports: [ReactiveFormsModule, NgxFormErrorDirective],\n" +
|
|
256
|
+
" template: `\n" +
|
|
257
|
+
" <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n" +
|
|
258
|
+
" <label>\n" +
|
|
259
|
+
" Email\n" +
|
|
260
|
+
" <input formControlName=\"email\" />\n" +
|
|
261
|
+
" <span ngxFormError=\"email\" [form]=\"form\"></span>\n" +
|
|
262
|
+
" </label>\n" +
|
|
263
|
+
"\n" +
|
|
264
|
+
" <label>\n" +
|
|
265
|
+
" Name\n" +
|
|
266
|
+
" <input formControlName=\"name\" />\n" +
|
|
267
|
+
" <span ngxFormError=\"name\" [form]=\"form\"></span>\n" +
|
|
268
|
+
" </label>\n" +
|
|
269
|
+
"\n" +
|
|
270
|
+
" <button type=\"submit\">Save</button>\n" +
|
|
271
|
+
" </form>\n" +
|
|
272
|
+
" `,\n" +
|
|
273
|
+
"})\n" +
|
|
274
|
+
"export class ApiFormsExampleComponent {\n" +
|
|
275
|
+
" private http = inject(HttpClient);\n" +
|
|
276
|
+
" private fb = inject(FormBuilder);\n" +
|
|
277
|
+
"\n" +
|
|
278
|
+
" form = this.fb.group({\n" +
|
|
279
|
+
" email: ['', [Validators.required, Validators.email]],\n" +
|
|
280
|
+
" name: ['', [Validators.required, Validators.minLength(3)]],\n" +
|
|
281
|
+
" });\n" +
|
|
282
|
+
"\n" +
|
|
283
|
+
" bridge = provideFormBridge(this.form, {\n" +
|
|
284
|
+
" preset: " + config.call + ",\n" +
|
|
285
|
+
" });\n" +
|
|
286
|
+
"\n" +
|
|
287
|
+
" onSubmit() {\n" +
|
|
288
|
+
" if (this.form.invalid) return;\n" +
|
|
289
|
+
"\n" +
|
|
290
|
+
" this.http.post('/api/example', this.form.value).subscribe({\n" +
|
|
291
|
+
" next: () => console.log('Success'),\n" +
|
|
292
|
+
" error: (err) => this.bridge.applyApiErrors(err.error),\n" +
|
|
293
|
+
" });\n" +
|
|
294
|
+
" }\n" +
|
|
295
|
+
"}\n"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
exports.ngAdd = ngAdd;
|
|
300
|
+
exports.injectInterceptor = injectInterceptor;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Node.js test for ng-add schematic (injectInterceptor)
|
|
2
|
+
// Run: node --test projects/ngx-api-forms/schematics/ng-add/index.spec.js
|
|
3
|
+
|
|
4
|
+
const { describe, it } = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const { injectInterceptor } = require('./index');
|
|
7
|
+
|
|
8
|
+
describe('injectInterceptor', () => {
|
|
9
|
+
it('should add interceptor to existing withInterceptors array', () => {
|
|
10
|
+
const input = [
|
|
11
|
+
"import { provideHttpClient, withInterceptors } from '@angular/common/http';",
|
|
12
|
+
'',
|
|
13
|
+
'export const appConfig = {',
|
|
14
|
+
' providers: [',
|
|
15
|
+
' provideHttpClient(withInterceptors([myInterceptor]))',
|
|
16
|
+
' ]',
|
|
17
|
+
'};',
|
|
18
|
+
].join('\n');
|
|
19
|
+
|
|
20
|
+
const result = injectInterceptor(input);
|
|
21
|
+
assert.ok(result.includes('apiErrorInterceptor()'));
|
|
22
|
+
assert.ok(result.includes('myInterceptor, apiErrorInterceptor()'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should add withInterceptors to bare provideHttpClient()', () => {
|
|
26
|
+
const input = [
|
|
27
|
+
"import { provideHttpClient } from '@angular/common/http';",
|
|
28
|
+
'',
|
|
29
|
+
'export const appConfig = {',
|
|
30
|
+
' providers: [',
|
|
31
|
+
' provideHttpClient()',
|
|
32
|
+
' ]',
|
|
33
|
+
'};',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
const result = injectInterceptor(input);
|
|
37
|
+
assert.ok(result.includes('withInterceptors([apiErrorInterceptor()])'));
|
|
38
|
+
assert.ok(result.includes('provideHttpClient(withInterceptors'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should add provideHttpClient when only providers array exists', () => {
|
|
42
|
+
const input = [
|
|
43
|
+
"import { ApplicationConfig } from '@angular/core';",
|
|
44
|
+
'',
|
|
45
|
+
'export const appConfig: ApplicationConfig = {',
|
|
46
|
+
' providers: [',
|
|
47
|
+
' provideZonelessChangeDetection(),',
|
|
48
|
+
' ]',
|
|
49
|
+
'};',
|
|
50
|
+
].join('\n');
|
|
51
|
+
|
|
52
|
+
const result = injectInterceptor(input);
|
|
53
|
+
assert.ok(result.includes('provideHttpClient(withInterceptors([apiErrorInterceptor()]))'));
|
|
54
|
+
assert.ok(result.includes("import { apiErrorInterceptor } from 'ngx-api-forms'"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not modify if apiErrorInterceptor already present', () => {
|
|
58
|
+
const input = [
|
|
59
|
+
"import { apiErrorInterceptor } from 'ngx-api-forms';",
|
|
60
|
+
"import { provideHttpClient, withInterceptors } from '@angular/common/http';",
|
|
61
|
+
'',
|
|
62
|
+
'export const appConfig = {',
|
|
63
|
+
' providers: [',
|
|
64
|
+
' provideHttpClient(withInterceptors([apiErrorInterceptor()]))',
|
|
65
|
+
' ]',
|
|
66
|
+
'};',
|
|
67
|
+
].join('\n');
|
|
68
|
+
|
|
69
|
+
const result = injectInterceptor(input);
|
|
70
|
+
assert.equal(result, input);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should add to existing ngx-api-forms import', () => {
|
|
74
|
+
const input = [
|
|
75
|
+
"import { provideFormBridge } from 'ngx-api-forms';",
|
|
76
|
+
"import { provideHttpClient, withInterceptors } from '@angular/common/http';",
|
|
77
|
+
'',
|
|
78
|
+
'export const appConfig = {',
|
|
79
|
+
' providers: [',
|
|
80
|
+
' provideHttpClient(withInterceptors([]))',
|
|
81
|
+
' ]',
|
|
82
|
+
'};',
|
|
83
|
+
].join('\n');
|
|
84
|
+
|
|
85
|
+
const result = injectInterceptor(input);
|
|
86
|
+
assert.ok(result.includes('provideFormBridge, apiErrorInterceptor'));
|
|
87
|
+
assert.ok(result.includes('apiErrorInterceptor()'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle provideHttpClient with existing args', () => {
|
|
91
|
+
const input = [
|
|
92
|
+
"import { provideHttpClient, withFetch } from '@angular/common/http';",
|
|
93
|
+
'',
|
|
94
|
+
'export const appConfig = {',
|
|
95
|
+
' providers: [',
|
|
96
|
+
' provideHttpClient(withFetch())',
|
|
97
|
+
' ]',
|
|
98
|
+
'};',
|
|
99
|
+
].join('\n');
|
|
100
|
+
|
|
101
|
+
const result = injectInterceptor(input);
|
|
102
|
+
assert.ok(result.includes('provideHttpClient(withFetch(), withInterceptors([apiErrorInterceptor()]))'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should add HTTP imports when missing', () => {
|
|
106
|
+
const input = [
|
|
107
|
+
"import { ApplicationConfig } from '@angular/core';",
|
|
108
|
+
'',
|
|
109
|
+
'export const appConfig: ApplicationConfig = {',
|
|
110
|
+
' providers: [',
|
|
111
|
+
' provideZonelessChangeDetection(),',
|
|
112
|
+
' ]',
|
|
113
|
+
'};',
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
116
|
+
const result = injectInterceptor(input);
|
|
117
|
+
assert.ok(result.includes("import { provideHttpClient, withInterceptors } from '@angular/common/http'"));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"id": "ngx-api-forms-ng-add",
|
|
4
|
+
"title": "Add ngx-api-forms",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"project": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The name of the project to add ngx-api-forms to.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "projectName"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"preset": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "The backend preset to generate.",
|
|
17
|
+
"default": "class-validator",
|
|
18
|
+
"enum": ["laravel", "django", "class-validator", "zod", "express-validator", "analog"],
|
|
19
|
+
"x-prompt": {
|
|
20
|
+
"message": "Which backend preset do you want to use?",
|
|
21
|
+
"type": "list",
|
|
22
|
+
"items": [
|
|
23
|
+
{ "value": "class-validator", "label": "NestJS / class-validator" },
|
|
24
|
+
{ "value": "laravel", "label": "Laravel" },
|
|
25
|
+
{ "value": "django", "label": "Django REST Framework" },
|
|
26
|
+
{ "value": "express-validator", "label": "Express / express-validator" },
|
|
27
|
+
{ "value": "zod", "label": "Zod (tRPC, Remix, etc.)" },
|
|
28
|
+
{ "value": "analog", "label": "Analog (Nitro/h3 backend)" }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": []
|
|
34
|
+
}
|
package/zod/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ErrorPreset, ConstraintMap } from 'ngx-api-forms';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod error preset.
|
|
5
|
+
*
|
|
6
|
+
* Parses errors from `ZodError.flatten()`:
|
|
7
|
+
* ```json
|
|
8
|
+
* {
|
|
9
|
+
* "formErrors": [],
|
|
10
|
+
* "fieldErrors": {
|
|
11
|
+
* "email": ["Invalid email"],
|
|
12
|
+
* "name": ["String must contain at least 3 character(s)"]
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Also supports raw `ZodError.issues`:
|
|
18
|
+
* ```json
|
|
19
|
+
* [
|
|
20
|
+
* { "code": "too_small", "minimum": 3, "path": ["name"], "message": "..." },
|
|
21
|
+
* { "code": "invalid_string", "validation": "email", "path": ["email"], "message": "..." }
|
|
22
|
+
* ]
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default constraint map for Zod.
|
|
28
|
+
*/
|
|
29
|
+
declare const ZOD_CONSTRAINT_MAP: ConstraintMap;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a Zod error preset.
|
|
32
|
+
*
|
|
33
|
+
* Supports both `.flatten()` and raw `.issues` formats.
|
|
34
|
+
*
|
|
35
|
+
* @param options.noInference - When true, skips constraint guessing entirely.
|
|
36
|
+
* The raw error message is used directly and the constraint is set to `'serverError'`.
|
|
37
|
+
* Useful for custom or translated Zod error messages.
|
|
38
|
+
* @param options.constraintPatterns - Custom regex patterns for constraint inference.
|
|
39
|
+
* Keys are constraint names, values are RegExp tested against the raw message.
|
|
40
|
+
* Checked before the built-in English patterns (flattened format only;
|
|
41
|
+
* the raw issues format uses structured `code` fields which are language-independent).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { zodPreset } from 'ngx-api-forms/zod';
|
|
46
|
+
*
|
|
47
|
+
* const bridge = createFormBridge(form, { preset: zodPreset() });
|
|
48
|
+
*
|
|
49
|
+
* // No inference: raw messages, no guessing
|
|
50
|
+
* const bridge = createFormBridge(form, { preset: zodPreset({ noInference: true }) });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function zodPreset(options?: {
|
|
54
|
+
noInference?: boolean;
|
|
55
|
+
constraintPatterns?: Record<string, RegExp>;
|
|
56
|
+
}): ErrorPreset;
|
|
57
|
+
|
|
58
|
+
export { ZOD_CONSTRAINT_MAP, zodPreset };
|