svelte-reflector 2.1.8 → 2.5.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/dist/core/Reflector.js +2 -1
- package/dist/core/api/ApiClassBuilder.js +12 -12
- package/dist/core/config/ReflectorConfig.d.ts +3 -1
- package/dist/core/config/ReflectorConfig.js +1 -0
- package/dist/core/generators/ApiCallStrategy.d.ts +1 -0
- package/dist/core/generators/ApiCallStrategy.js +10 -1
- package/dist/core/generators/CallMethodGenerator.js +6 -2
- package/dist/core/generators/CallStrategy.d.ts +6 -1
- package/dist/core/generators/ModuleCallStrategy.d.ts +1 -0
- package/dist/core/generators/ModuleCallStrategy.js +10 -1
- package/dist/core/module/Module.d.ts +2 -0
- package/dist/core/module/Module.js +4 -1
- package/dist/core/module/ModuleMethodProcessor.d.ts +2 -0
- package/dist/core/module/ModuleMethodProcessor.js +3 -0
- package/dist/core/module/ModuleSchemaFileBuilder.js +15 -1
- package/dist/core/schema/Schema.d.ts +16 -0
- package/dist/core/schema/Schema.js +33 -0
- package/dist/core/schema/SchemaClassRenderer.d.ts +2 -0
- package/dist/core/schema/SchemaClassRenderer.js +29 -3
- package/dist/core/schema/SchemaDependencyCollector.d.ts +1 -0
- package/dist/core/schema/SchemaDependencyCollector.js +4 -0
- package/dist/core/schema/SchemaPropertyClassifier.js +4 -0
- package/dist/core/schema/SchemaRegistry.d.ts +2 -0
- package/dist/core/schema/SchemaRegistry.js +10 -1
- package/dist/helpers/generate-doc.helper.d.ts +1 -0
- package/dist/helpers/generate-doc.helper.js +5 -0
- package/dist/loaders/ConfigLoader.js +2 -0
- package/dist/props/array.property.d.ts +1 -0
- package/dist/props/array.property.js +6 -0
- package/dist/props/enum.property.d.ts +1 -0
- package/dist/props/enum.property.js +4 -0
- package/dist/props/object.property.d.ts +1 -0
- package/dist/props/object.property.js +13 -0
- package/dist/props/primitive.property.d.ts +3 -0
- package/dist/props/primitive.property.js +25 -6
- package/dist/runtime/reflector.svelte.ts +255 -42
- package/dist/types/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/core/Reflector.js
CHANGED
|
@@ -33,7 +33,8 @@ export class Reflector {
|
|
|
33
33
|
context: this.context,
|
|
34
34
|
config: this.config,
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
const requestBodyNames = new Set(this.modules.flatMap((m) => m.requestBodyNames));
|
|
37
|
+
this.registry = new SchemaRegistry({ components, fieldConfigs, context: this.context, requestBodyNames });
|
|
37
38
|
this.schemas = this.registry.schemas;
|
|
38
39
|
this.propertiesNames = this.registry.propertiesNames;
|
|
39
40
|
}
|
|
@@ -53,17 +53,17 @@ export class ApiClassBuilder {
|
|
|
53
53
|
this.imports.addReflectorImport("genericArrayBundler");
|
|
54
54
|
}
|
|
55
55
|
const className = capitalizeFirstLetter(method.name);
|
|
56
|
-
const classCode = `
|
|
57
|
-
export class ${className} {
|
|
58
|
-
${stateProps.join(";")}
|
|
59
|
-
${processedParams.paramAttributes.map((a) => `${a};`).join("\n ")}
|
|
60
|
-
|
|
61
|
-
${callMethod}
|
|
62
|
-
|
|
63
|
-
reset() {
|
|
64
|
-
${resetLines.join(";")}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
56
|
+
const classCode = `
|
|
57
|
+
export class ${className} {
|
|
58
|
+
${stateProps.join(";")}
|
|
59
|
+
${processedParams.paramAttributes.map((a) => `${a};`).join("\n ")}
|
|
60
|
+
|
|
61
|
+
${callMethod}
|
|
62
|
+
|
|
63
|
+
reset() {
|
|
64
|
+
${resetLines.join(";")}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
67
|
`;
|
|
68
68
|
const paramCode = processedParams.paramClasses.join("\n");
|
|
69
69
|
return { paramCode, classCode, schemaEntries };
|
|
@@ -91,7 +91,7 @@ export class ApiClassBuilder {
|
|
|
91
91
|
const { attributeType, bodyType } = method.request;
|
|
92
92
|
const lines = [];
|
|
93
93
|
if (attributeType === "form" && bodyType) {
|
|
94
|
-
lines.push(`this.form
|
|
94
|
+
lines.push(`this.form.reset()`);
|
|
95
95
|
}
|
|
96
96
|
else if (attributeType === "list") {
|
|
97
97
|
lines.push("this.data = []");
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
export interface ReflectorConfig {
|
|
7
7
|
/** Alias that resolves to the generated reflector folder (e.g. `$reflector`). */
|
|
8
8
|
reflectorAlias: string;
|
|
9
|
-
/** Full import path to the validators
|
|
9
|
+
/** Full import path to the validators module (exports `validateInputs`). */
|
|
10
10
|
validatorsImport: string;
|
|
11
|
+
/** Full import path to the sanitizers module (input masks; exports `sanitizers`). */
|
|
12
|
+
sanitizersImport: string;
|
|
11
13
|
/** Module path for the environment flag (e.g. `$env/static/public`). */
|
|
12
14
|
environmentImport: string;
|
|
13
15
|
/** Name of the exported environment flag — values other than `DEV` are treated as prod. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const DEFAULT_REFLECTOR_CONFIG = {
|
|
2
2
|
reflectorAlias: "$reflector",
|
|
3
3
|
validatorsImport: "$lib/sanitizers/validateFormats",
|
|
4
|
+
sanitizersImport: "$lib/sanitizers/input-sanitizers",
|
|
4
5
|
environmentImport: "$env/static/public",
|
|
5
6
|
environmentFlag: "PUBLIC_ENVIRONMENT",
|
|
6
7
|
toastImport: "$lib/utils/toast.svelte",
|
|
@@ -2,6 +2,7 @@ import type { CallMethodInput, CallStrategy } from "./CallStrategy.js";
|
|
|
2
2
|
export declare class ApiCallStrategy implements CallStrategy {
|
|
3
3
|
listStateAccess(_method: CallMethodInput): string;
|
|
4
4
|
buildSignature(method: CallMethodInput): string;
|
|
5
|
+
buildLegacyWrapper(method: CallMethodInput): string;
|
|
5
6
|
entityStateAccess(_method: CallMethodInput): string;
|
|
6
7
|
formStateAccess(_method: CallMethodInput): string;
|
|
7
8
|
private buildParamsType;
|
|
@@ -5,7 +5,16 @@ export class ApiCallStrategy {
|
|
|
5
5
|
}
|
|
6
6
|
buildSignature(method) {
|
|
7
7
|
const paramsType = this.buildParamsType(method);
|
|
8
|
-
return `async
|
|
8
|
+
return `async run(params?: ${paramsType})`;
|
|
9
|
+
}
|
|
10
|
+
buildLegacyWrapper(method) {
|
|
11
|
+
const paramsType = this.buildParamsType(method);
|
|
12
|
+
return `
|
|
13
|
+
/** @deprecated use \`run()\` — returns a discriminated ApiResult */
|
|
14
|
+
async call(params?: ${paramsType}) {
|
|
15
|
+
const res = await this.run(params);
|
|
16
|
+
return res.ok ? res.data : undefined;
|
|
17
|
+
}`;
|
|
9
18
|
}
|
|
10
19
|
entityStateAccess(_method) {
|
|
11
20
|
return "this.data";
|
|
@@ -8,6 +8,7 @@ export class CallMethodGenerator {
|
|
|
8
8
|
const { inside, outside } = this.buildApiCall(method, strategy);
|
|
9
9
|
const methodReturn = this.buildMethodReturn(method, strategy);
|
|
10
10
|
const signature = strategy.buildSignature(method);
|
|
11
|
+
const legacyWrapper = strategy.buildLegacyWrapper(method);
|
|
11
12
|
return `
|
|
12
13
|
${description}
|
|
13
14
|
${signature} {
|
|
@@ -25,7 +26,7 @@ export class CallMethodGenerator {
|
|
|
25
26
|
${inside}
|
|
26
27
|
await onSuccess?.(response);
|
|
27
28
|
|
|
28
|
-
return ${methodReturn};
|
|
29
|
+
return { ok: true, data: ${methodReturn} };
|
|
29
30
|
} catch (e) {
|
|
30
31
|
let parsedError: ApiErrorResponse;
|
|
31
32
|
try {
|
|
@@ -33,11 +34,14 @@ export class CallMethodGenerator {
|
|
|
33
34
|
} catch {
|
|
34
35
|
parsedError = { error: 'unknown', message: (e as Error).message ?? String(e) };
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
await onError?.(parsedError);
|
|
38
|
+
return { ok: false, error: parsedError };
|
|
37
39
|
} finally {
|
|
38
40
|
this.loading = false;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
|
|
44
|
+
${legacyWrapper}
|
|
41
45
|
`;
|
|
42
46
|
}
|
|
43
47
|
buildProps(method) {
|
|
@@ -17,8 +17,13 @@ export interface CallMethodInput {
|
|
|
17
17
|
* (per-endpoint Api class vs Module `_methodName` protected method).
|
|
18
18
|
*/
|
|
19
19
|
export interface CallStrategy {
|
|
20
|
-
/** Full
|
|
20
|
+
/** Full signature of the discriminated `run` method incl. params —
|
|
21
|
+
* e.g. `async run(params?: ...)` (api) or `protected async _fooRun(params?: ...)` (module). */
|
|
21
22
|
buildSignature(method: CallMethodInput): string;
|
|
23
|
+
/** Full `@deprecated` legacy method that delegates to the `run` variant and
|
|
24
|
+
* reproduces the old `Res | null | undefined` return shape. Emitted verbatim
|
|
25
|
+
* after the run method. Api → `call()`; module → `_foo()`. */
|
|
26
|
+
buildLegacyWrapper(method: CallMethodInput): string;
|
|
22
27
|
/** State field holding list results — e.g. `this.list` / `this.listControllers` (module) or `this.data` (api).
|
|
23
28
|
* Takes `method` so the module strategy can suffix the field when two list
|
|
24
29
|
* operations collide in the same controller. */
|
|
@@ -2,6 +2,7 @@ import type { CallMethodInput, CallStrategy } from "./CallStrategy.js";
|
|
|
2
2
|
export declare class ModuleCallStrategy implements CallStrategy {
|
|
3
3
|
listStateAccess(method: CallMethodInput): string;
|
|
4
4
|
buildSignature(method: CallMethodInput): string;
|
|
5
|
+
buildLegacyWrapper(method: CallMethodInput): string;
|
|
5
6
|
entityStateAccess(method: CallMethodInput): string;
|
|
6
7
|
formStateAccess(method: CallMethodInput): string;
|
|
7
8
|
private buildParamsType;
|
|
@@ -6,7 +6,16 @@ export class ModuleCallStrategy {
|
|
|
6
6
|
}
|
|
7
7
|
buildSignature(method) {
|
|
8
8
|
const paramsType = this.buildParamsType(method);
|
|
9
|
-
return `protected async _${method.name}(params?: ${paramsType})`;
|
|
9
|
+
return `protected async _${method.name}Run(params?: ${paramsType})`;
|
|
10
|
+
}
|
|
11
|
+
buildLegacyWrapper(method) {
|
|
12
|
+
const paramsType = this.buildParamsType(method);
|
|
13
|
+
return `
|
|
14
|
+
/** @deprecated use \`_${method.name}Run()\` — returns a discriminated ApiResult */
|
|
15
|
+
protected async _${method.name}(params?: ${paramsType}) {
|
|
16
|
+
const res = await this._${method.name}Run(params);
|
|
17
|
+
return res.ok ? res.data : undefined;
|
|
18
|
+
}`;
|
|
10
19
|
}
|
|
11
20
|
entityStateAccess(method) {
|
|
12
21
|
const rType = method.analyzers.request.responseType ?? "";
|
|
@@ -12,6 +12,8 @@ export declare class Module {
|
|
|
12
12
|
readonly methods: Method[];
|
|
13
13
|
/** Schema class names directly used by this module (for per-module schema generation) */
|
|
14
14
|
readonly schemaClassNames: string[];
|
|
15
|
+
/** Nomes-raiz dos request body DTOs deste módulo (para serialização schema-aware) */
|
|
16
|
+
readonly requestBodyNames: string[];
|
|
15
17
|
private readonly imports;
|
|
16
18
|
private readonly methodProcessor;
|
|
17
19
|
private readonly classBuilder;
|
|
@@ -14,6 +14,8 @@ export class Module {
|
|
|
14
14
|
methods;
|
|
15
15
|
/** Schema class names directly used by this module (for per-module schema generation) */
|
|
16
16
|
schemaClassNames;
|
|
17
|
+
/** Nomes-raiz dos request body DTOs deste módulo (para serialização schema-aware) */
|
|
18
|
+
requestBodyNames;
|
|
17
19
|
imports;
|
|
18
20
|
methodProcessor;
|
|
19
21
|
classBuilder;
|
|
@@ -55,6 +57,7 @@ export class Module {
|
|
|
55
57
|
// Extract schema class names for per-module schema generation
|
|
56
58
|
this.schemaClassNames = Array.from(processedMethods.entries)
|
|
57
59
|
.filter((e) => e !== "type any" && !e.startsWith("type "));
|
|
60
|
+
this.requestBodyNames = Array.from(processedMethods.requestBodyNames);
|
|
58
61
|
// Monta o resultado final
|
|
59
62
|
const allBuilded = this.buildModuleData(processedMethods, processedParams);
|
|
60
63
|
const moduleConstructor = this.constructorBuilder.build(allBuilded.form);
|
|
@@ -97,7 +100,7 @@ export class Module {
|
|
|
97
100
|
`);
|
|
98
101
|
moduleInit.add("this.clearForms()");
|
|
99
102
|
moduleClear.add(`
|
|
100
|
-
protected clearForms() {
|
|
103
|
+
protected clearForms() { ${form.map((f) => `this.forms.${f.name}.reset()`).join("; ")} }
|
|
101
104
|
`);
|
|
102
105
|
}
|
|
103
106
|
return {
|
|
@@ -6,6 +6,8 @@ import type { Form } from "./ModuleConstructorBuilder.js";
|
|
|
6
6
|
export interface ProcessedMethods {
|
|
7
7
|
buildedMethods: string[];
|
|
8
8
|
entries: Set<string>;
|
|
9
|
+
/** Nomes-raiz dos request body DTOs (para serialização schema-aware via bundleInputs) */
|
|
10
|
+
requestBodyNames: Set<string>;
|
|
9
11
|
form: Form[];
|
|
10
12
|
formSet: Set<string>;
|
|
11
13
|
methodsAttributes: string[];
|
|
@@ -13,6 +13,7 @@ export class ModuleMethodProcessor {
|
|
|
13
13
|
const form = [];
|
|
14
14
|
const formSet = new Set();
|
|
15
15
|
const entries = new Set();
|
|
16
|
+
const requestBodyNames = new Set();
|
|
16
17
|
const buildedMethods = [];
|
|
17
18
|
const queryMap = new Map();
|
|
18
19
|
const headerMap = new Map();
|
|
@@ -54,6 +55,7 @@ export class ModuleMethodProcessor {
|
|
|
54
55
|
}
|
|
55
56
|
if (bodyType) {
|
|
56
57
|
entries.add(bodyType);
|
|
58
|
+
requestBodyNames.add(bodyType);
|
|
57
59
|
}
|
|
58
60
|
if (responseType && responseType !== "response" && !isPrimitiveResponse) {
|
|
59
61
|
entries.add(`type ${responseType}Interface`);
|
|
@@ -64,6 +66,7 @@ export class ModuleMethodProcessor {
|
|
|
64
66
|
form,
|
|
65
67
|
formSet,
|
|
66
68
|
entries,
|
|
69
|
+
requestBodyNames,
|
|
67
70
|
buildedMethods,
|
|
68
71
|
methodsAttributes: Array.from(methodsAttributes),
|
|
69
72
|
methodsInit: Array.from(methodsInit),
|
|
@@ -16,11 +16,25 @@ export class ModuleSchemaFileBuilder {
|
|
|
16
16
|
customTypeDeps.add(t);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
// só presença importa (o import é o objeto `sanitizers` inteiro), não os refs
|
|
20
|
+
const usesSanitizers = schemas.some((s) => s.sanitizerDeps.length > 0);
|
|
19
21
|
const treatedSchemas = schemas.map((s) => `${s.interface};\n${s.schema};`);
|
|
22
|
+
// import preciso: response usa bundleStrict, request usa bundleInputs, array-root
|
|
23
|
+
// não usa nenhum. Evita import morto (svelte-check/eslint do consumer reclama).
|
|
24
|
+
const needsStrict = schemas.some((s) => s.bundleHelper === "strict");
|
|
25
|
+
const needsInputs = schemas.some((s) => s.bundleHelper === "inputs");
|
|
26
|
+
const reflectorImports = ["build", "BuildedInput"];
|
|
27
|
+
if (needsStrict)
|
|
28
|
+
reflectorImports.push("bundleStrict");
|
|
29
|
+
if (needsInputs)
|
|
30
|
+
reflectorImports.push("bundleInputs");
|
|
20
31
|
const imports = [
|
|
21
|
-
`import {
|
|
32
|
+
`import { ${reflectorImports.join(", ")} } from "${config.reflectorAlias}/reflector.svelte";`,
|
|
22
33
|
`import { validateInputs } from "${config.validatorsImport}";`,
|
|
23
34
|
];
|
|
35
|
+
if (usesSanitizers) {
|
|
36
|
+
imports.push(`import { sanitizers } from "${config.sanitizersImport}";`);
|
|
37
|
+
}
|
|
24
38
|
if (enumDeps.size > 0) {
|
|
25
39
|
imports.push(`import type { ${[...enumDeps].join(", ")} } from "${config.reflectorAlias}/enums"`);
|
|
26
40
|
}
|
|
@@ -17,8 +17,16 @@ export declare class Schema {
|
|
|
17
17
|
readonly enumDeps: string[];
|
|
18
18
|
/** Custom type names used by this schema (from fieldConfigs) */
|
|
19
19
|
readonly customTypeDeps: string[];
|
|
20
|
+
/** Sanitizer refs used by this schema (from fieldConfigs) — gates the `sanitizers` import */
|
|
21
|
+
readonly sanitizerDeps: string[];
|
|
20
22
|
schema: string;
|
|
21
23
|
interface: string;
|
|
24
|
+
/**
|
|
25
|
+
* Qual helper de runtime o `bundle()` renderizado referencia — controla os imports
|
|
26
|
+
* do schema file. `'strict'` = response (bundleStrict), `'inputs'` = request
|
|
27
|
+
* (bundleInputs), `null` = array-root (mapeia `item.bundle()`, não usa nenhum).
|
|
28
|
+
*/
|
|
29
|
+
bundleHelper: "strict" | "inputs" | null;
|
|
22
30
|
/**
|
|
23
31
|
* Builds a Schema for an array-root component (top-level `type: array`), e.g.
|
|
24
32
|
* a promoted `data: array` response envelope. Renders a wrapper class whose
|
|
@@ -48,4 +56,12 @@ export declare class Schema {
|
|
|
48
56
|
fieldConfigs: FieldConfigs;
|
|
49
57
|
context: CodegenContext;
|
|
50
58
|
});
|
|
59
|
+
/**
|
|
60
|
+
* Re-renderiza este schema como request DTO: `bundle()` passa a serializar a partir
|
|
61
|
+
* das instâncias `BuildedInput` via `bundleInputs` (em vez de `bundleStrict` sobre os
|
|
62
|
+
* `.value` extraídos). Idempotente. Schemas array-root não têm props → no-op
|
|
63
|
+
* (continuam mapeando `item.bundle()`). Chamado pelo `SchemaRegistry` para o fecho
|
|
64
|
+
* transitivo dos request bodies.
|
|
65
|
+
*/
|
|
66
|
+
setRequestMode(): void;
|
|
51
67
|
}
|
|
@@ -19,8 +19,16 @@ export class Schema {
|
|
|
19
19
|
enumDeps;
|
|
20
20
|
/** Custom type names used by this schema (from fieldConfigs) */
|
|
21
21
|
customTypeDeps;
|
|
22
|
+
/** Sanitizer refs used by this schema (from fieldConfigs) — gates the `sanitizers` import */
|
|
23
|
+
sanitizerDeps;
|
|
22
24
|
schema;
|
|
23
25
|
interface;
|
|
26
|
+
/**
|
|
27
|
+
* Qual helper de runtime o `bundle()` renderizado referencia — controla os imports
|
|
28
|
+
* do schema file. `'strict'` = response (bundleStrict), `'inputs'` = request
|
|
29
|
+
* (bundleInputs), `null` = array-root (mapeia `item.bundle()`, não usa nenhum).
|
|
30
|
+
*/
|
|
31
|
+
bundleHelper;
|
|
24
32
|
/**
|
|
25
33
|
* Builds a Schema for an array-root component (top-level `type: array`), e.g.
|
|
26
34
|
* a promoted `data: array` response envelope. Renders a wrapper class whose
|
|
@@ -40,6 +48,7 @@ export class Schema {
|
|
|
40
48
|
schema.schemaDeps = element.kind === "ref" ? [element.type] : [];
|
|
41
49
|
schema.enumDeps = element.kind === "enum" ? [element.type] : [];
|
|
42
50
|
schema.customTypeDeps = [];
|
|
51
|
+
schema.sanitizerDeps = [];
|
|
43
52
|
const rendered = ArraySchemaRenderer.render({
|
|
44
53
|
name,
|
|
45
54
|
elementType: element.type,
|
|
@@ -47,6 +56,7 @@ export class Schema {
|
|
|
47
56
|
});
|
|
48
57
|
schema.interface = rendered.interface;
|
|
49
58
|
schema.schema = rendered.schema;
|
|
59
|
+
schema.bundleHelper = null;
|
|
50
60
|
return schema;
|
|
51
61
|
}
|
|
52
62
|
/**
|
|
@@ -110,6 +120,7 @@ export class Schema {
|
|
|
110
120
|
this.schemaDeps = deps.schemaDeps;
|
|
111
121
|
this.enumDeps = deps.enumDeps;
|
|
112
122
|
this.customTypeDeps = deps.customTypeDeps;
|
|
123
|
+
this.sanitizerDeps = deps.sanitizerDeps;
|
|
113
124
|
const rendered = SchemaClassRenderer.render({
|
|
114
125
|
name: this.name,
|
|
115
126
|
primitiveProps: this.primitiveProps,
|
|
@@ -119,5 +130,27 @@ export class Schema {
|
|
|
119
130
|
});
|
|
120
131
|
this.interface = rendered.interface;
|
|
121
132
|
this.schema = rendered.schema;
|
|
133
|
+
this.bundleHelper = rendered.bundleHelper;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Re-renderiza este schema como request DTO: `bundle()` passa a serializar a partir
|
|
137
|
+
* das instâncias `BuildedInput` via `bundleInputs` (em vez de `bundleStrict` sobre os
|
|
138
|
+
* `.value` extraídos). Idempotente. Schemas array-root não têm props → no-op
|
|
139
|
+
* (continuam mapeando `item.bundle()`). Chamado pelo `SchemaRegistry` para o fecho
|
|
140
|
+
* transitivo dos request bodies.
|
|
141
|
+
*/
|
|
142
|
+
setRequestMode() {
|
|
143
|
+
if (this.bundleHelper !== "strict")
|
|
144
|
+
return;
|
|
145
|
+
const rendered = SchemaClassRenderer.render({
|
|
146
|
+
name: this.name,
|
|
147
|
+
primitiveProps: this.primitiveProps,
|
|
148
|
+
arrayProps: this.arrayProps,
|
|
149
|
+
objectProps: this.objectProps,
|
|
150
|
+
enumProps: this.enumProps,
|
|
151
|
+
mode: "request",
|
|
152
|
+
});
|
|
153
|
+
this.schema = rendered.schema;
|
|
154
|
+
this.bundleHelper = rendered.bundleHelper;
|
|
122
155
|
}
|
|
123
156
|
}
|
|
@@ -9,8 +9,10 @@ export declare class SchemaClassRenderer {
|
|
|
9
9
|
arrayProps: ArrayProp[];
|
|
10
10
|
objectProps: ObjectProp[];
|
|
11
11
|
enumProps: EnumProp[];
|
|
12
|
+
mode?: "request" | "response";
|
|
12
13
|
}): {
|
|
13
14
|
interface: string;
|
|
14
15
|
schema: string;
|
|
16
|
+
bundleHelper: "strict" | "inputs";
|
|
15
17
|
};
|
|
16
18
|
}
|
|
@@ -2,6 +2,7 @@ import { ReflectorInterface } from "./ReflectorInterface.js";
|
|
|
2
2
|
export class SchemaClassRenderer {
|
|
3
3
|
static render(params) {
|
|
4
4
|
const { name, primitiveProps, arrayProps, objectProps, enumProps } = params;
|
|
5
|
+
const mode = params.mode ?? "response";
|
|
5
6
|
const reflectorInterface = new ReflectorInterface({
|
|
6
7
|
name,
|
|
7
8
|
arrayProps,
|
|
@@ -34,9 +35,32 @@ export class SchemaClassRenderer {
|
|
|
34
35
|
keys.push(prop.classBuild());
|
|
35
36
|
bundleParams.push(prop.bundleBuild());
|
|
36
37
|
});
|
|
37
|
-
const constructorCode = `constructor(params?: { data?: ${name}Interface | undefined, empty?: boolean }) {
|
|
38
|
+
const constructorCode = `constructor(params?: { data?: ${name}Interface | undefined, empty?: boolean }) {
|
|
38
39
|
${constructorThis.join(";\n")}
|
|
39
40
|
}`;
|
|
41
|
+
const hydrateLines = [];
|
|
42
|
+
primitiveProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
43
|
+
arrayProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
44
|
+
objectProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
45
|
+
enumProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
46
|
+
const hydrateCode = `
|
|
47
|
+
hydrate(data: Partial<${name}Interface>): void {
|
|
48
|
+
${hydrateLines.join(";\n")}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reset(): void {
|
|
52
|
+
this.hydrate(new ${name}({ empty: true }).bundle() as Partial<${name}Interface>);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
// Request DTO serializa a partir das instâncias `BuildedInput` (carregam os flags
|
|
56
|
+
// required/nullable), não do `.value` pré-extraído — bundleInputs faz a coerção
|
|
57
|
+
// nullable ''→null e cobre array/aninhado. Response fica em bundleStrict (inalterado).
|
|
58
|
+
const bundleHelper = mode === "request" ? "inputs" : "strict";
|
|
59
|
+
const bundleBody = mode === "request"
|
|
60
|
+
? `return bundleInputs({ ${[...primitiveProps, ...arrayProps, ...objectProps, ...enumProps]
|
|
61
|
+
.map((p) => `${p.name}: this.${p.name}`)
|
|
62
|
+
.join(",")} })`
|
|
63
|
+
: `return bundleStrict({ ${bundleParams.join(",")} })`;
|
|
40
64
|
const schema = `
|
|
41
65
|
export class ${name} {
|
|
42
66
|
${keys.join(";")}
|
|
@@ -45,10 +69,12 @@ export class SchemaClassRenderer {
|
|
|
45
69
|
|
|
46
70
|
${staticMethod}
|
|
47
71
|
|
|
72
|
+
${hydrateCode}
|
|
73
|
+
|
|
48
74
|
bundle(){
|
|
49
|
-
|
|
75
|
+
${bundleBody}
|
|
50
76
|
}
|
|
51
77
|
};`;
|
|
52
|
-
return { interface: reflectorInterface.builded, schema };
|
|
78
|
+
return { interface: reflectorInterface.builded, schema, bundleHelper };
|
|
53
79
|
}
|
|
54
80
|
}
|
|
@@ -20,14 +20,18 @@ export class SchemaDependencyCollector {
|
|
|
20
20
|
enumDepsSet.add(prop.type);
|
|
21
21
|
}
|
|
22
22
|
const customTypeDepsSet = new Set();
|
|
23
|
+
const sanitizerDepsSet = new Set();
|
|
23
24
|
for (const prop of primitiveProps) {
|
|
24
25
|
if (prop.customType)
|
|
25
26
|
customTypeDepsSet.add(prop.customType);
|
|
27
|
+
if (prop.sanitizer)
|
|
28
|
+
sanitizerDepsSet.add(prop.sanitizer);
|
|
26
29
|
}
|
|
27
30
|
return {
|
|
28
31
|
schemaDeps: [...schemaDepsSet],
|
|
29
32
|
enumDeps: [...enumDepsSet],
|
|
30
33
|
customTypeDeps: [...customTypeDepsSet],
|
|
34
|
+
sanitizerDeps: [...sanitizerDepsSet],
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -15,6 +15,7 @@ export class SchemaPropertyClassifier {
|
|
|
15
15
|
schemaObject: fakeStringSchema,
|
|
16
16
|
required: requireds.includes(key),
|
|
17
17
|
validator: config?.validator,
|
|
18
|
+
sanitizer: config?.sanitizer,
|
|
18
19
|
customType: config?.type,
|
|
19
20
|
isParam: undefined,
|
|
20
21
|
isNullable: value.nullable,
|
|
@@ -53,6 +54,7 @@ export class SchemaPropertyClassifier {
|
|
|
53
54
|
}
|
|
54
55
|
const config = fieldConfigs.get(key);
|
|
55
56
|
const validator = config?.validator;
|
|
57
|
+
const sanitizer = config?.sanitizer;
|
|
56
58
|
const customType = config?.type;
|
|
57
59
|
const type = value.type;
|
|
58
60
|
if (type === "object") {
|
|
@@ -63,6 +65,7 @@ export class SchemaPropertyClassifier {
|
|
|
63
65
|
schemaObject: fakeStringSchema,
|
|
64
66
|
required,
|
|
65
67
|
validator,
|
|
68
|
+
sanitizer,
|
|
66
69
|
customType,
|
|
67
70
|
isParam: undefined,
|
|
68
71
|
isNullable: value.nullable,
|
|
@@ -87,6 +90,7 @@ export class SchemaPropertyClassifier {
|
|
|
87
90
|
schemaObject: value,
|
|
88
91
|
required,
|
|
89
92
|
validator,
|
|
93
|
+
sanitizer,
|
|
90
94
|
customType,
|
|
91
95
|
isParam: undefined,
|
|
92
96
|
isNullable: value.nullable,
|
|
@@ -10,6 +10,8 @@ export declare class SchemaRegistry {
|
|
|
10
10
|
components: ComponentsObject;
|
|
11
11
|
fieldConfigs: FieldConfigs;
|
|
12
12
|
context: CodegenContext;
|
|
13
|
+
/** Nomes-raiz dos request body DTOs — eles e seu fecho transitivo serializam via bundleInputs */
|
|
14
|
+
requestBodyNames?: Set<string>;
|
|
13
15
|
});
|
|
14
16
|
/** Resolve transitive schema dependencies from a list of schema class names */
|
|
15
17
|
resolveTransitiveDeps(names: string[]): Schema[];
|
|
@@ -5,7 +5,7 @@ export class SchemaRegistry {
|
|
|
5
5
|
propertiesNames = new Set();
|
|
6
6
|
schemaMap = new Map();
|
|
7
7
|
constructor(params) {
|
|
8
|
-
const { components, fieldConfigs, context } = params;
|
|
8
|
+
const { components, fieldConfigs, context, requestBodyNames } = params;
|
|
9
9
|
const componentSchemas = components.schemas;
|
|
10
10
|
if (!componentSchemas)
|
|
11
11
|
return;
|
|
@@ -32,6 +32,15 @@ export class SchemaRegistry {
|
|
|
32
32
|
for (const schema of this.schemas) {
|
|
33
33
|
this.schemaMap.set(schema.name, schema);
|
|
34
34
|
}
|
|
35
|
+
// Request body DTOs (e seu fecho transitivo de deps) serializam via bundleInputs:
|
|
36
|
+
// o bundle() lê os flags do BuildedInput em vez de só o .value extraído. Transitivo
|
|
37
|
+
// porque um DTO aninhado (ex.: createBody.role → UserRole) também é serializado no
|
|
38
|
+
// request — marcar só a raiz deixaria o nested em bundleStrict (bug nested).
|
|
39
|
+
if (requestBodyNames && requestBodyNames.size > 0) {
|
|
40
|
+
for (const schema of this.resolveTransitiveDeps([...requestBodyNames])) {
|
|
41
|
+
schema.setRequestMode();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
35
44
|
console.log(`${this.schemas.length} schemas generated successfully.`);
|
|
36
45
|
}
|
|
37
46
|
/** Resolve transitive schema dependencies from a list of schema class names */
|
|
@@ -96,12 +96,17 @@ export function parseFieldConfigsFromConfig(code) {
|
|
|
96
96
|
// extrai validator (referência sem aspas, ex: validateInputs.email)
|
|
97
97
|
const validatorMatch = block.match(/validator\s*:\s*([A-Za-z0-9_$.]+)/);
|
|
98
98
|
const validator = validatorMatch?.[1]?.trim();
|
|
99
|
+
// extrai sanitizer (referência sem aspas, ex: sanitizers.phone)
|
|
100
|
+
const sanitizerMatch = block.match(/sanitizer\s*:\s*([A-Za-z0-9_$.]+)/);
|
|
101
|
+
const sanitizer = sanitizerMatch?.[1]?.trim();
|
|
99
102
|
// extrai type (string literal com aspas, ex: 'IconName')
|
|
100
103
|
const typeMatch = block.match(/type\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
101
104
|
const type = typeMatch?.[1]?.trim();
|
|
102
105
|
const config = { fields };
|
|
103
106
|
if (validator)
|
|
104
107
|
config.validator = validator;
|
|
108
|
+
if (sanitizer)
|
|
109
|
+
config.sanitizer = sanitizer;
|
|
105
110
|
if (type)
|
|
106
111
|
config.type = type;
|
|
107
112
|
results.push(config);
|
|
@@ -96,6 +96,12 @@ export class ArrayProp {
|
|
|
96
96
|
.join(", ")}]`;
|
|
97
97
|
return `readonly ${this.name} = new EnumQueryBuilder<${this.type}>({ key: '${this.name}', defaultValues: ${literal} })`;
|
|
98
98
|
}
|
|
99
|
+
hydrateBuild() {
|
|
100
|
+
if (this.isSchemaRef) {
|
|
101
|
+
return `if (data.${this.name} !== undefined) this.${this.name} = (data.${this.name} ?? []).map((i) => new ${this.type}({ data: i as never }))`;
|
|
102
|
+
}
|
|
103
|
+
return `if (data.${this.name} !== undefined) this.${this.name} = data.${this.name} ?? []`;
|
|
104
|
+
}
|
|
99
105
|
staticBuild() {
|
|
100
106
|
const result = this._isPrimitiveType ? "obj" : `new ${this.type}({ data: obj })`;
|
|
101
107
|
const aType = this._isPrimitiveType ? this.type : `${this.type}Interface`;
|
|
@@ -39,4 +39,8 @@ export class EnumProp {
|
|
|
39
39
|
bundleBuild() {
|
|
40
40
|
return `${this.name}: this.${this.name}?.value`;
|
|
41
41
|
}
|
|
42
|
+
hydrateBuild() {
|
|
43
|
+
const opt = this.isRequired ? "" : "?";
|
|
44
|
+
return `if (data.${this.name} !== undefined) this.${this.name}${opt}.hydrate(data.${this.name} as never)`;
|
|
45
|
+
}
|
|
42
46
|
}
|
|
@@ -31,4 +31,17 @@ export class ObjectProp {
|
|
|
31
31
|
const nullable = this.isNullable ? "?? null" : "";
|
|
32
32
|
return `${this.name}: this.${this.name}?.bundle() ${nullable}`;
|
|
33
33
|
}
|
|
34
|
+
hydrateBuild() {
|
|
35
|
+
if (this.isNullable) {
|
|
36
|
+
return `if (data.${this.name} !== undefined) {
|
|
37
|
+
if (data.${this.name} === null) this.${this.name} = null;
|
|
38
|
+
else if (this.${this.name}) this.${this.name}.hydrate(data.${this.name} as never);
|
|
39
|
+
else this.${this.name} = new ${this.type}({ data: data.${this.name} as never });
|
|
40
|
+
}`;
|
|
41
|
+
}
|
|
42
|
+
return `if (data.${this.name} !== undefined) {
|
|
43
|
+
if (this.${this.name}) this.${this.name}.hydrate(data.${this.name} as never);
|
|
44
|
+
else this.${this.name} = new ${this.type}({ data: data.${this.name} as never });
|
|
45
|
+
}`;
|
|
46
|
+
}
|
|
34
47
|
}
|
|
@@ -10,6 +10,7 @@ export declare class PrimitiveProp {
|
|
|
10
10
|
private readonly isNullable;
|
|
11
11
|
readonly rawType: ReflectorParamType;
|
|
12
12
|
readonly customType: string | undefined;
|
|
13
|
+
readonly sanitizer: string | undefined;
|
|
13
14
|
private readonly buildedConst;
|
|
14
15
|
private readonly example;
|
|
15
16
|
private readonly fallbackExample;
|
|
@@ -23,6 +24,7 @@ export declare class PrimitiveProp {
|
|
|
23
24
|
schemaObject: SchemaObject;
|
|
24
25
|
required: boolean;
|
|
25
26
|
validator: string | undefined;
|
|
27
|
+
sanitizer?: string | undefined;
|
|
26
28
|
customType?: string | undefined;
|
|
27
29
|
isParam: boolean | undefined;
|
|
28
30
|
isNullable?: boolean | undefined;
|
|
@@ -38,5 +40,6 @@ export declare class PrimitiveProp {
|
|
|
38
40
|
queryBuild(): string;
|
|
39
41
|
updateQueryBuild(): string;
|
|
40
42
|
bundleBuild(): string;
|
|
43
|
+
hydrateBuild(): string;
|
|
41
44
|
}
|
|
42
45
|
export {};
|
|
@@ -9,6 +9,7 @@ export class PrimitiveProp {
|
|
|
9
9
|
isNullable;
|
|
10
10
|
rawType;
|
|
11
11
|
customType;
|
|
12
|
+
sanitizer;
|
|
12
13
|
buildedConst;
|
|
13
14
|
example;
|
|
14
15
|
fallbackExample;
|
|
@@ -22,7 +23,7 @@ export class PrimitiveProp {
|
|
|
22
23
|
return this.isNullable || !this.required;
|
|
23
24
|
}
|
|
24
25
|
constructor(params) {
|
|
25
|
-
const { name, schemaObject, required, validator, customType, isParam, isNullable } = params;
|
|
26
|
+
const { name, schemaObject, required, validator, sanitizer, customType, isParam, isNullable } = params;
|
|
26
27
|
const { type: rawType } = schemaObject;
|
|
27
28
|
this.isNullable = !!isNullable;
|
|
28
29
|
const normalizedRawType = rawType === "integer" ? "number" : rawType;
|
|
@@ -38,10 +39,13 @@ export class PrimitiveProp {
|
|
|
38
39
|
this.isSpecial = treated.isSpecial;
|
|
39
40
|
this.rawType = type ?? "any";
|
|
40
41
|
this.customType = customType;
|
|
42
|
+
// sanitizer só vale para campo string — gate aqui garante que emissão e
|
|
43
|
+
// rastreio de dependência (import) concordam.
|
|
44
|
+
this.sanitizer = type === "string" ? sanitizer : undefined;
|
|
41
45
|
this.type = `BuildedInput<${buildedType}>`;
|
|
42
46
|
this.required = required;
|
|
43
47
|
this.isParam = !!isParam;
|
|
44
|
-
this.buildedConst = this.buildConst({ example, name: this.name, required, type, validator, emptyExample: this.example });
|
|
48
|
+
this.buildedConst = this.buildConst({ example, name: this.name, required, type, validator, sanitizer: this.sanitizer, emptyExample: this.example });
|
|
45
49
|
}
|
|
46
50
|
getEmptyExample(params) {
|
|
47
51
|
const { schemaObject, type, name } = params;
|
|
@@ -83,7 +87,7 @@ export class PrimitiveProp {
|
|
|
83
87
|
};
|
|
84
88
|
}
|
|
85
89
|
buildConst(params) {
|
|
86
|
-
const { name, required, type, validator } = params;
|
|
90
|
+
const { name, required, type, validator, sanitizer } = params;
|
|
87
91
|
const getValidator = (type) => {
|
|
88
92
|
if (type === "string") {
|
|
89
93
|
return "emptyString";
|
|
@@ -114,10 +118,22 @@ export class PrimitiveProp {
|
|
|
114
118
|
else if (this.customType) {
|
|
115
119
|
typeParam = `<${this.effectiveType}>`;
|
|
116
120
|
}
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
// Monta as props num array e junta com vírgula: com dois opcionais
|
|
122
|
+
// (validator + sanitizer) o controle manual de vírgula quebrava.
|
|
123
|
+
const props = [
|
|
124
|
+
`key: ${keyExpr}`,
|
|
125
|
+
`placeholder: ${this.example}`,
|
|
126
|
+
`example: ${buildedExample}`,
|
|
127
|
+
`required: ${required}`,
|
|
128
|
+
this.isNullable ? "nullable: true" : "",
|
|
129
|
+
this.max !== undefined ? `max: ${this.max}` : "",
|
|
130
|
+
buildedValidator(),
|
|
131
|
+
sanitizer ? `sanitizer: ${sanitizer}` : "",
|
|
132
|
+
]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join(", ");
|
|
119
135
|
return `
|
|
120
|
-
build${typeParam}({
|
|
136
|
+
build${typeParam}({ ${props} })
|
|
121
137
|
`;
|
|
122
138
|
}
|
|
123
139
|
thisDot() {
|
|
@@ -154,4 +170,7 @@ export class PrimitiveProp {
|
|
|
154
170
|
bundleBuild() {
|
|
155
171
|
return `${this.name}: ${this.thisDot()}${this.name}?.value`;
|
|
156
172
|
}
|
|
173
|
+
hydrateBuild() {
|
|
174
|
+
return `if (data.${this.name} !== undefined) ${this.thisDot()}${this.name}.hydrate(data.${this.name} as never)`;
|
|
175
|
+
}
|
|
157
176
|
}
|
|
@@ -3,12 +3,18 @@ import toast from "$lib/utils/toast.svelte";
|
|
|
3
3
|
import { goto } from "$app/navigation";
|
|
4
4
|
import { page } from "$app/state";
|
|
5
5
|
import { browser } from "$app/environment";
|
|
6
|
+
import { untrack } from "svelte";
|
|
6
7
|
import { SvelteURL } from "svelte/reactivity";
|
|
7
8
|
|
|
8
9
|
type ValidatorResult = string | null;
|
|
9
10
|
type ValidatorFn<T> = (v: T) => ValidatorResult;
|
|
10
11
|
type BundleResult<T> = T extends { bundle: () => infer R } ? R : T;
|
|
11
12
|
|
|
13
|
+
export type Sanitizer = {
|
|
14
|
+
parse: (display: string) => string; // texto exibido -> valor canônico
|
|
15
|
+
format: (value: string) => string; // valor canônico -> texto exibido (máscara)
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
export type ApiCallParams<TResponse, TPaths = void, TQuery = void> = {
|
|
13
19
|
behavior?: Behavior<TResponse, ApiErrorResponse>;
|
|
14
20
|
} & (TPaths extends void ? object : { paths?: TPaths }) &
|
|
@@ -30,9 +36,18 @@ export type SvelteEvent = {
|
|
|
30
36
|
currentTarget: EventTarget & SeiLa;
|
|
31
37
|
};
|
|
32
38
|
|
|
39
|
+
export interface ValidationErrorItem {
|
|
40
|
+
field: string;
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
received?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
export interface ApiErrorResponse {
|
|
34
47
|
error: string;
|
|
35
48
|
message: string;
|
|
49
|
+
statusCode?: number;
|
|
50
|
+
errors?: ValidationErrorItem[];
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export class Behavior<TSuccess = unknown, TError = unknown> {
|
|
@@ -40,9 +55,26 @@ export class Behavior<TSuccess = unknown, TError = unknown> {
|
|
|
40
55
|
onSuccess?: (v: TSuccess) => Promise<void> | void;
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Discriminated result of a generated `run()` call. `ok` is the single
|
|
60
|
+
* discriminator — never branch on `data` being nullish to detect success.
|
|
61
|
+
* `data` is the raw response class instance (with `.bundle()`) for endpoints
|
|
62
|
+
* with a body, and `null` for void endpoints.
|
|
63
|
+
*/
|
|
64
|
+
export type ApiResult<T> =
|
|
65
|
+
| { ok: true; data: T }
|
|
66
|
+
| { ok: false; error: ApiErrorResponse };
|
|
67
|
+
|
|
43
68
|
export class BuildedInput<T> {
|
|
44
|
-
value = $state<T>(null as any);
|
|
45
69
|
display = $state<T>(null as any);
|
|
70
|
+
// Backing store for `value` — only used WITHOUT a sanitizer, where `value`
|
|
71
|
+
// and `display` are two independent states (legacy behavior). With a
|
|
72
|
+
// sanitizer, `display` is the single writable source and `value` derives
|
|
73
|
+
// from it via the getter, so `_value` is left untouched.
|
|
74
|
+
private _value = $state<T>(null as any);
|
|
75
|
+
private serverErrorMessage = $state<string | null>(null);
|
|
76
|
+
private serverErrorValue = $state.raw<T | null>(null);
|
|
77
|
+
sanitizer?: Sanitizer;
|
|
46
78
|
required: boolean;
|
|
47
79
|
nullable: boolean;
|
|
48
80
|
placeholder: T;
|
|
@@ -58,13 +90,10 @@ export class BuildedInput<T> {
|
|
|
58
90
|
placeholder: T;
|
|
59
91
|
max?: number;
|
|
60
92
|
validator?: ValidatorFn<T>;
|
|
93
|
+
sanitizer?: Sanitizer;
|
|
61
94
|
}) {
|
|
62
|
-
const { example, required, nullable, key, validator, placeholder, max } = params;
|
|
95
|
+
const { example, required, nullable, key, validator, placeholder, max, sanitizer } = params;
|
|
63
96
|
|
|
64
|
-
const initial = key === undefined ? example : key;
|
|
65
|
-
|
|
66
|
-
this.value = initial;
|
|
67
|
-
this.display = initial;
|
|
68
97
|
this.required = required;
|
|
69
98
|
this.nullable = nullable ?? false;
|
|
70
99
|
this.placeholder = placeholder;
|
|
@@ -76,12 +105,95 @@ export class BuildedInput<T> {
|
|
|
76
105
|
if (validator) {
|
|
77
106
|
this.validator = validator;
|
|
78
107
|
}
|
|
108
|
+
|
|
109
|
+
if (sanitizer) {
|
|
110
|
+
this.sanitizer = sanitizer;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const initial = key === undefined ? example : key;
|
|
114
|
+
|
|
115
|
+
if (this.sanitizer) {
|
|
116
|
+
this.value = initial as T; // setter formata display a partir do valor canônico
|
|
117
|
+
} else {
|
|
118
|
+
this._value = initial; // comportamento atual: dois states independentes
|
|
119
|
+
this.display = initial;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get value(): T {
|
|
124
|
+
if (!this.sanitizer) return this._value;
|
|
125
|
+
const parsed = this.sanitizer.parse((this.display ?? "") as unknown as string);
|
|
126
|
+
if (this.nullable && parsed === "") return null as unknown as T;
|
|
127
|
+
return parsed as unknown as T;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set value(v: T) {
|
|
131
|
+
if (!this.sanitizer) {
|
|
132
|
+
// Antes só escrevia `_value`, deixando `display` stale → input de texto
|
|
133
|
+
// (`bind:value={data.display}`) renderizava vazio quando o consumidor setava
|
|
134
|
+
// `.value` e esquecia `.display`. Agora mantém os dois em sincronia. `_value`
|
|
135
|
+
// segue sendo a fonte de leitura do getter, então não quebra quem mantém
|
|
136
|
+
// value≠display via sanitizer ou mask manual (que seta `display` DEPOIS).
|
|
137
|
+
this._value = v;
|
|
138
|
+
this.display = v;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (v === null || v === undefined) {
|
|
142
|
+
this.display = "" as unknown as T;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.display = this.sanitizer.format(String(v)) as unknown as T;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reaplica a máscara: parse(display) -> format. Usado na hidratação / oninput
|
|
150
|
+
* pelo componente. No-op sem sanitizer. Reassina `display`, então o caret vai
|
|
151
|
+
* pro fim — isso é responsabilidade do componente, não do reflector.
|
|
152
|
+
*/
|
|
153
|
+
reformat(): void {
|
|
154
|
+
if (!this.sanitizer) return;
|
|
155
|
+
const parsed = this.sanitizer.parse((this.display ?? "") as unknown as string);
|
|
156
|
+
this.display = this.sanitizer.format(parsed) as unknown as T;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Hidrata in-place: seta valor canônico + display formatado, sanitizer-aware,
|
|
161
|
+
* e limpa server error. Envolto em `untrack` pra poder ser chamado de dentro
|
|
162
|
+
* de um `$effect` que lê `data` sem criar loop (a leitura de
|
|
163
|
+
* `display`/`sanitizer` não vira dependência). Substitui o
|
|
164
|
+
* `hydrateForm`/`clearFormInPlace` que os consumidores escreviam na mão.
|
|
165
|
+
*/
|
|
166
|
+
hydrate(v: T): void {
|
|
167
|
+
untrack(() => {
|
|
168
|
+
if (this.sanitizer) {
|
|
169
|
+
this.value = v; // setter já formata display a partir do canônico
|
|
170
|
+
} else {
|
|
171
|
+
this._value = v;
|
|
172
|
+
this.display = v;
|
|
173
|
+
}
|
|
174
|
+
this.clearServerError();
|
|
175
|
+
});
|
|
79
176
|
}
|
|
80
177
|
|
|
81
178
|
validate(): ValidatorResult {
|
|
82
179
|
if (!this.validator) return null;
|
|
83
180
|
return this.validator(this.value);
|
|
84
181
|
}
|
|
182
|
+
|
|
183
|
+
setServerError(message: string) {
|
|
184
|
+
this.serverErrorMessage = message;
|
|
185
|
+
this.serverErrorValue = this.value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
clearServerError() {
|
|
189
|
+
this.serverErrorMessage = null;
|
|
190
|
+
this.serverErrorValue = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
get serverError(): string | null {
|
|
194
|
+
if (this.serverErrorMessage === null) return null;
|
|
195
|
+
return this.value === this.serverErrorValue ? this.serverErrorMessage : null;
|
|
196
|
+
}
|
|
85
197
|
}
|
|
86
198
|
|
|
87
199
|
export class EnumQueryBuilder<T> {
|
|
@@ -89,8 +201,8 @@ export class EnumQueryBuilder<T> {
|
|
|
89
201
|
private readonly defaultValues: T[] = [];
|
|
90
202
|
|
|
91
203
|
values = $derived(
|
|
92
|
-
page.url.searchParams.has(this.key)
|
|
93
|
-
? (page.url.searchParams.getAll(this.key) as T[])
|
|
204
|
+
(pendingUrl ?? page.url).searchParams.has(this.key)
|
|
205
|
+
? ((pendingUrl ?? page.url).searchParams.getAll(this.key) as T[])
|
|
94
206
|
: this.defaultValues,
|
|
95
207
|
);
|
|
96
208
|
selected = $state<T | null>(null);
|
|
@@ -124,6 +236,7 @@ export function build<T>(params: {
|
|
|
124
236
|
nullable?: boolean;
|
|
125
237
|
max?: number;
|
|
126
238
|
validator?: ValidatorFn<T>;
|
|
239
|
+
sanitizer?: Sanitizer;
|
|
127
240
|
}): BuildedInput<T> {
|
|
128
241
|
return new BuildedInput(params);
|
|
129
242
|
}
|
|
@@ -165,19 +278,45 @@ export function genericArrayBundler<T extends { bundle: () => BundleResult<T> }>
|
|
|
165
278
|
return (data as T[]).map((item) => item.bundle());
|
|
166
279
|
}
|
|
167
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Acumulador de mutações de query param. N chamadas no mesmo tick mutam a MESMA
|
|
283
|
+
* `SvelteURL` pendente e coalescem num único `goto` agendado por microtask —
|
|
284
|
+
* evita clobber (cada `goto` partir da URL antiga) e faz read-after-write
|
|
285
|
+
* honesto (os getters leem de `pendingUrl ?? page.url`). `$state.raw`: a troca
|
|
286
|
+
* de referência null↔URL dá a reatividade grossa; a `SvelteURL` já é reativa
|
|
287
|
+
* nos próprios `searchParams`, então um `$state` profundo proxiaria um objeto
|
|
288
|
+
* que já é reativo.
|
|
289
|
+
*/
|
|
290
|
+
let pendingUrl = $state.raw<SvelteURL | null>(null);
|
|
291
|
+
|
|
292
|
+
function stageParamMutation(mutate: (params: URLSearchParams) => void) {
|
|
293
|
+
if (!browser) return;
|
|
294
|
+
if (!pendingUrl) {
|
|
295
|
+
pendingUrl = new SvelteURL(page.url);
|
|
296
|
+
queueMicrotask(flushPendingUrl);
|
|
297
|
+
}
|
|
298
|
+
mutate(pendingUrl.searchParams);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function flushPendingUrl() {
|
|
302
|
+
const url = pendingUrl;
|
|
303
|
+
pendingUrl = null;
|
|
304
|
+
if (url) goto(url, { replaceState: true, keepFocus: true });
|
|
305
|
+
}
|
|
306
|
+
|
|
168
307
|
/**
|
|
169
308
|
* Atualiza um query param na URL.
|
|
170
309
|
* - `""` (string vazia) → remove o param.
|
|
171
310
|
* - qualquer outro valor → `searchParams.set(key, String(event))`.
|
|
172
311
|
*/
|
|
173
312
|
export function changeParam({ event, key }: QueryContract) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
313
|
+
stageParamMutation((params) => {
|
|
314
|
+
if (event === "") {
|
|
315
|
+
params.delete(key);
|
|
316
|
+
} else {
|
|
317
|
+
params.set(key, String(event));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
181
320
|
}
|
|
182
321
|
|
|
183
322
|
type StringOrNumber = string | number;
|
|
@@ -200,11 +339,21 @@ export class QueryBuilder {
|
|
|
200
339
|
: String(params.defaultValue);
|
|
201
340
|
}
|
|
202
341
|
|
|
203
|
-
|
|
204
|
-
|
|
342
|
+
/** Snapshot read-only do query param (a URL é a fonte). Escrita só via `.update()`. */
|
|
343
|
+
get current(): string | null {
|
|
344
|
+
const fromUrl = (pendingUrl ?? page.url).searchParams.get(this.key);
|
|
205
345
|
return fromUrl !== null ? fromUrl : this.defaultValue;
|
|
206
346
|
}
|
|
207
347
|
|
|
348
|
+
/**
|
|
349
|
+
* @deprecated Use `.current`. `.value` colide de nome com `BuildedInput.value`
|
|
350
|
+
* (que é gravável) — semântica oposta, fonte de confusão. Será removido num major
|
|
351
|
+
* futuro; até lá delega pra `.current`.
|
|
352
|
+
*/
|
|
353
|
+
get value(): string | null {
|
|
354
|
+
return this.current;
|
|
355
|
+
}
|
|
356
|
+
|
|
208
357
|
/**
|
|
209
358
|
* Aplica o valor recebido ao query param.
|
|
210
359
|
* - `null` / `undefined` → no-op (não chama `goto`).
|
|
@@ -224,35 +373,31 @@ export class QueryBuilder {
|
|
|
224
373
|
* - outros valores → `set(key, String(value))`.
|
|
225
374
|
*/
|
|
226
375
|
export function setQueryGroup(group: QueryWithArrayType[]) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
376
|
+
stageParamMutation((params) => {
|
|
377
|
+
for (const p of group) {
|
|
378
|
+
const { key, value } = p;
|
|
379
|
+
|
|
380
|
+
if (Array.isArray(value)) {
|
|
381
|
+
params.delete(key);
|
|
382
|
+
value.forEach((v) => params.append(key, String(v)));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (value === "") {
|
|
387
|
+
params.delete(key);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
params.set(key, String(value));
|
|
238
392
|
}
|
|
239
|
-
|
|
240
|
-
if (value === "") {
|
|
241
|
-
url.searchParams.delete(key);
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
url.searchParams.set(key, String(value));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
goto(url, { replaceState: true, keepFocus: false });
|
|
393
|
+
});
|
|
249
394
|
}
|
|
250
395
|
|
|
251
396
|
export function changeArrayParam({ values, key }: { values: string[]; key: string }) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
397
|
+
stageParamMutation((params) => {
|
|
398
|
+
params.delete(key);
|
|
399
|
+
values.forEach((value) => params.append(key, value));
|
|
400
|
+
});
|
|
256
401
|
}
|
|
257
402
|
|
|
258
403
|
export function bundleStrict<T extends Record<string, unknown>>(
|
|
@@ -261,3 +406,71 @@ export function bundleStrict<T extends Record<string, unknown>>(
|
|
|
261
406
|
export function bundleStrict(payload: Record<string, unknown>): Record<string, unknown> {
|
|
262
407
|
return Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== undefined));
|
|
263
408
|
}
|
|
409
|
+
|
|
410
|
+
function isBuildedInput(v: unknown): v is BuildedInput<unknown> {
|
|
411
|
+
return v != null && typeof v === "object" && (v as { kind?: unknown }).kind === "builded";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Serialização schema-aware de request: recebe as instâncias `BuildedInput` (não o
|
|
416
|
+
* `.value` já extraído), então enxerga os flags (`required`/`nullable`) que vivem na
|
|
417
|
+
* instância. Corrige o 400 silencioso do `bundleStrict` cego: campo `nullable` apagado
|
|
418
|
+
* pra `''` virava `""` no payload (estourava parse de ISO date no back). Aqui `nullable`
|
|
419
|
+
* com `''` vira `null` de propósito.
|
|
420
|
+
*
|
|
421
|
+
* Não faz gate client-side de `required` — consistente com o padrão do projeto
|
|
422
|
+
* (`bundle()` só serializa; validação é do backend, surface via toast). Quem quiser
|
|
423
|
+
* gate síncrono usa `isFormValid` antes do `bundle`.
|
|
424
|
+
*
|
|
425
|
+
* Trata cada entry: `undefined` → omite; `BuildedInput` → value (com coerção nullable);
|
|
426
|
+
* array → `genericArrayBundler`; DTO aninhado (`.bundle()`) → recursa; plain → passthrough.
|
|
427
|
+
*
|
|
428
|
+
* O overload tipado espelha o `bundleStrict`: strip do wrapper `BuildedInput<V> → V`,
|
|
429
|
+
* array → `BundleResult`, DTO aninhado → retorno do seu `bundle`, e dropa as keys
|
|
430
|
+
* puramente `undefined`. Sem ele o request `bundle()` regredia pra `Record<string,unknown>`
|
|
431
|
+
* e estourava svelte-check em todo consumidor que lê campo tipado do payload.
|
|
432
|
+
*/
|
|
433
|
+
type BundledValue<V> =
|
|
434
|
+
V extends BuildedInput<infer U>
|
|
435
|
+
? U
|
|
436
|
+
: V extends readonly (infer E)[]
|
|
437
|
+
? BundleResult<E>[]
|
|
438
|
+
: V extends { bundle: () => infer R }
|
|
439
|
+
? R
|
|
440
|
+
: V;
|
|
441
|
+
|
|
442
|
+
export function bundleInputs<T extends Record<string, unknown>>(
|
|
443
|
+
inputs: T,
|
|
444
|
+
): {
|
|
445
|
+
[K in keyof T as Exclude<BundledValue<T[K]>, undefined> extends never
|
|
446
|
+
? never
|
|
447
|
+
: K]: Exclude<BundledValue<T[K]>, undefined>;
|
|
448
|
+
};
|
|
449
|
+
export function bundleInputs(inputs: Record<string, unknown>): Record<string, unknown>;
|
|
450
|
+
export function bundleInputs(inputs: Record<string, unknown>): Record<string, unknown> {
|
|
451
|
+
const out: Record<string, unknown> = {};
|
|
452
|
+
for (const [key, v] of Object.entries(inputs)) {
|
|
453
|
+
if (v === undefined) continue; // opcional nunca construído → omite
|
|
454
|
+
|
|
455
|
+
if (isBuildedInput(v)) {
|
|
456
|
+
let val = v.value as unknown;
|
|
457
|
+
if (v.nullable && val === "") val = null; // mata o ''→"" que estoura ISO no back
|
|
458
|
+
if (val === undefined) continue; // omite undefined; NÃO omite null (nullable manda null de propósito)
|
|
459
|
+
out[key] = val;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (Array.isArray(v)) {
|
|
464
|
+
out[key] = genericArrayBundler(v);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (v && typeof (v as { bundle?: unknown }).bundle === "function") {
|
|
469
|
+
out[key] = (v as { bundle: () => unknown }).bundle(); // DTO aninhado → recursa
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
out[key] = v; // plain (v !== undefined já garantido)
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
package/dist/types/types.d.ts
CHANGED