svelte-reflector 2.1.9 → 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.
Files changed (38) hide show
  1. package/dist/core/Reflector.js +2 -1
  2. package/dist/core/api/ApiClassBuilder.js +12 -12
  3. package/dist/core/config/ReflectorConfig.d.ts +3 -1
  4. package/dist/core/config/ReflectorConfig.js +1 -0
  5. package/dist/core/generators/ApiCallStrategy.d.ts +1 -0
  6. package/dist/core/generators/ApiCallStrategy.js +10 -1
  7. package/dist/core/generators/CallMethodGenerator.js +6 -2
  8. package/dist/core/generators/CallStrategy.d.ts +6 -1
  9. package/dist/core/generators/ModuleCallStrategy.d.ts +1 -0
  10. package/dist/core/generators/ModuleCallStrategy.js +10 -1
  11. package/dist/core/module/Module.d.ts +2 -0
  12. package/dist/core/module/Module.js +4 -1
  13. package/dist/core/module/ModuleMethodProcessor.d.ts +2 -0
  14. package/dist/core/module/ModuleMethodProcessor.js +3 -0
  15. package/dist/core/module/ModuleSchemaFileBuilder.js +15 -1
  16. package/dist/core/schema/Schema.d.ts +16 -0
  17. package/dist/core/schema/Schema.js +33 -0
  18. package/dist/core/schema/SchemaClassRenderer.d.ts +2 -0
  19. package/dist/core/schema/SchemaClassRenderer.js +29 -3
  20. package/dist/core/schema/SchemaDependencyCollector.d.ts +1 -0
  21. package/dist/core/schema/SchemaDependencyCollector.js +4 -0
  22. package/dist/core/schema/SchemaPropertyClassifier.js +4 -0
  23. package/dist/core/schema/SchemaRegistry.d.ts +2 -0
  24. package/dist/core/schema/SchemaRegistry.js +10 -1
  25. package/dist/helpers/generate-doc.helper.d.ts +1 -0
  26. package/dist/helpers/generate-doc.helper.js +5 -0
  27. package/dist/loaders/ConfigLoader.js +2 -0
  28. package/dist/props/array.property.d.ts +1 -0
  29. package/dist/props/array.property.js +6 -0
  30. package/dist/props/enum.property.d.ts +1 -0
  31. package/dist/props/enum.property.js +4 -0
  32. package/dist/props/object.property.d.ts +1 -0
  33. package/dist/props/object.property.js +13 -0
  34. package/dist/props/primitive.property.d.ts +3 -0
  35. package/dist/props/primitive.property.js +25 -6
  36. package/dist/runtime/reflector.svelte.ts +229 -42
  37. package/dist/types/types.d.ts +1 -0
  38. package/package.json +1 -1
@@ -33,7 +33,8 @@ export class Reflector {
33
33
  context: this.context,
34
34
  config: this.config,
35
35
  });
36
- this.registry = new SchemaRegistry({ components, fieldConfigs, context: this.context });
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 = new ${bodyType}()`);
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/sanitizers module. */
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 call(params?: ${paramsType})`;
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
- return await onError?.(parsedError);
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 method signature incl. params e.g. `async call(params?: ...)` or `protected async _foo(params?: ...)` */
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() { this.forms = this.buildForms(true) }
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 { build, BuildedInput, bundleStrict } from "${config.reflectorAlias}/reflector.svelte";`,
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
- return bundleStrict({ ${bundleParams.join(",")} })
75
+ ${bundleBody}
50
76
  }
51
77
  };`;
52
- return { interface: reflectorInterface.builded, schema };
78
+ return { interface: reflectorInterface.builded, schema, bundleHelper };
53
79
  }
54
80
  }
@@ -6,6 +6,7 @@ export interface SchemaDependencies {
6
6
  schemaDeps: string[];
7
7
  enumDeps: string[];
8
8
  customTypeDeps: string[];
9
+ sanitizerDeps: string[];
9
10
  }
10
11
  export declare class SchemaDependencyCollector {
11
12
  static collect(props: {
@@ -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 */
@@ -1,6 +1,7 @@
1
1
  interface LooseFieldConfig {
2
2
  fields: string[];
3
3
  validator?: string;
4
+ sanitizer?: string;
4
5
  type?: string;
5
6
  }
6
7
  export declare function parseFieldConfigsFromConfig(code: string): LooseFieldConfig[];
@@ -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);
@@ -22,6 +22,8 @@ export class ConfigLoader {
22
22
  const config = {};
23
23
  if (rel.validator)
24
24
  config.validator = rel.validator;
25
+ if (rel.sanitizer)
26
+ config.sanitizer = rel.sanitizer;
25
27
  if (rel.type)
26
28
  config.type = rel.type;
27
29
  fieldConfigs.set(field, config);
@@ -28,5 +28,6 @@ export declare class ArrayProp {
28
28
  bundleBuild(): string;
29
29
  queryBundleBuild(): string;
30
30
  queryBuild(): string;
31
+ hydrateBuild(): string;
31
32
  staticBuild(): string;
32
33
  }
@@ -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`;
@@ -19,4 +19,5 @@ export declare class EnumProp {
19
19
  interfaceBuild(): string;
20
20
  queryBuild(): string;
21
21
  bundleBuild(): string;
22
+ hydrateBuild(): string;
22
23
  }
@@ -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
  }
@@ -14,4 +14,5 @@ export declare class ObjectProp {
14
14
  classBuild(): string;
15
15
  interfaceBuild(): string;
16
16
  bundleBuild(): string;
17
+ hydrateBuild(): string;
17
18
  }
@@ -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
- const nullableParam = this.isNullable ? "nullable: true, " : "";
118
- const maxParam = this.max !== undefined ? `max: ${this.max}, ` : "";
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}({ key: ${keyExpr}, placeholder: ${this.example}, example: ${buildedExample}, required: ${required}, ${nullableParam}${maxParam}${buildedValidator()}})
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 }) &
@@ -49,11 +55,26 @@ export class Behavior<TSuccess = unknown, TError = unknown> {
49
55
  onSuccess?: (v: TSuccess) => Promise<void> | void;
50
56
  }
51
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
+
52
68
  export class BuildedInput<T> {
53
- value = $state<T>(null as any);
54
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);
55
75
  private serverErrorMessage = $state<string | null>(null);
56
76
  private serverErrorValue = $state.raw<T | null>(null);
77
+ sanitizer?: Sanitizer;
57
78
  required: boolean;
58
79
  nullable: boolean;
59
80
  placeholder: T;
@@ -69,13 +90,10 @@ export class BuildedInput<T> {
69
90
  placeholder: T;
70
91
  max?: number;
71
92
  validator?: ValidatorFn<T>;
93
+ sanitizer?: Sanitizer;
72
94
  }) {
73
- const { example, required, nullable, key, validator, placeholder, max } = params;
95
+ const { example, required, nullable, key, validator, placeholder, max, sanitizer } = params;
74
96
 
75
- const initial = key === undefined ? example : key;
76
-
77
- this.value = initial;
78
- this.display = initial;
79
97
  this.required = required;
80
98
  this.nullable = nullable ?? false;
81
99
  this.placeholder = placeholder;
@@ -87,6 +105,74 @@ export class BuildedInput<T> {
87
105
  if (validator) {
88
106
  this.validator = validator;
89
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
+ });
90
176
  }
91
177
 
92
178
  validate(): ValidatorResult {
@@ -115,8 +201,8 @@ export class EnumQueryBuilder<T> {
115
201
  private readonly defaultValues: T[] = [];
116
202
 
117
203
  values = $derived(
118
- page.url.searchParams.has(this.key)
119
- ? (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[])
120
206
  : this.defaultValues,
121
207
  );
122
208
  selected = $state<T | null>(null);
@@ -150,6 +236,7 @@ export function build<T>(params: {
150
236
  nullable?: boolean;
151
237
  max?: number;
152
238
  validator?: ValidatorFn<T>;
239
+ sanitizer?: Sanitizer;
153
240
  }): BuildedInput<T> {
154
241
  return new BuildedInput(params);
155
242
  }
@@ -191,19 +278,45 @@ export function genericArrayBundler<T extends { bundle: () => BundleResult<T> }>
191
278
  return (data as T[]).map((item) => item.bundle());
192
279
  }
193
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
+
194
307
  /**
195
308
  * Atualiza um query param na URL.
196
309
  * - `""` (string vazia) → remove o param.
197
310
  * - qualquer outro valor → `searchParams.set(key, String(event))`.
198
311
  */
199
312
  export function changeParam({ event, key }: QueryContract) {
200
- const url = new SvelteURL(page.url);
201
- if (event === "") {
202
- url.searchParams.delete(key);
203
- } else {
204
- url.searchParams.set(key, String(event));
205
- }
206
- goto(url, { replaceState: true, keepFocus: true });
313
+ stageParamMutation((params) => {
314
+ if (event === "") {
315
+ params.delete(key);
316
+ } else {
317
+ params.set(key, String(event));
318
+ }
319
+ });
207
320
  }
208
321
 
209
322
  type StringOrNumber = string | number;
@@ -226,11 +339,21 @@ export class QueryBuilder {
226
339
  : String(params.defaultValue);
227
340
  }
228
341
 
229
- get value(): string | null {
230
- const fromUrl = page.url.searchParams.get(this.key);
342
+ /** Snapshot read-only do query param (a URL é a fonte). Escrita via `.update()`. */
343
+ get current(): string | null {
344
+ const fromUrl = (pendingUrl ?? page.url).searchParams.get(this.key);
231
345
  return fromUrl !== null ? fromUrl : this.defaultValue;
232
346
  }
233
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
+
234
357
  /**
235
358
  * Aplica o valor recebido ao query param.
236
359
  * - `null` / `undefined` → no-op (não chama `goto`).
@@ -250,35 +373,31 @@ export class QueryBuilder {
250
373
  * - outros valores → `set(key, String(value))`.
251
374
  */
252
375
  export function setQueryGroup(group: QueryWithArrayType[]) {
253
- if (!browser) return;
254
-
255
- const url = new SvelteURL(page.url);
256
-
257
- for (const p of group) {
258
- const { key, value } = p;
259
-
260
- if (Array.isArray(value)) {
261
- url.searchParams.delete(key);
262
- value.forEach((v) => url.searchParams.append(key, String(v)));
263
- continue;
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));
264
392
  }
265
-
266
- if (value === "") {
267
- url.searchParams.delete(key);
268
- continue;
269
- }
270
-
271
- url.searchParams.set(key, String(value));
272
- }
273
-
274
- goto(url, { replaceState: true, keepFocus: false });
393
+ });
275
394
  }
276
395
 
277
396
  export function changeArrayParam({ values, key }: { values: string[]; key: string }) {
278
- const url = new SvelteURL(page.url);
279
- url.searchParams.delete(key);
280
- values.forEach((value) => url.searchParams.append(key, value));
281
- goto(url, { replaceState: true, keepFocus: true });
397
+ stageParamMutation((params) => {
398
+ params.delete(key);
399
+ values.forEach((value) => params.append(key, value));
400
+ });
282
401
  }
283
402
 
284
403
  export function bundleStrict<T extends Record<string, unknown>>(
@@ -287,3 +406,71 @@ export function bundleStrict<T extends Record<string, unknown>>(
287
406
  export function bundleStrict(payload: Record<string, unknown>): Record<string, unknown> {
288
407
  return Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== undefined));
289
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
+ }
@@ -17,6 +17,7 @@ export type Info = {
17
17
  export type Example = string | boolean | number;
18
18
  export interface FieldConfig {
19
19
  validator?: string;
20
+ sanitizer?: string;
20
21
  type?: string;
21
22
  }
22
23
  export type FieldConfigs = Map<string, FieldConfig>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-reflector",
3
- "version": "2.1.9",
3
+ "version": "2.5.0",
4
4
  "description": "Reflects types from openAPI schemas",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",