@atscript/moost-validator 0.1.83 → 0.1.85

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/index.cjs CHANGED
@@ -26,6 +26,37 @@ const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"
26
26
  const moost = __toESM(require("moost"));
27
27
  const __moostjs_event_http = __toESM(require("@moostjs/event-http"));
28
28
 
29
+ //#region packages/moost-validator/src/as-coercion.pipe.ts
30
+ /**
31
+ * Param sources that arrive as strings over the wire and are safe to coerce
32
+ * by default. `BODY` is deliberately excluded — coercing JSON bodies silently
33
+ * loosens the API contract; opt in via {@link TCoercionOptions.sources}.
34
+ */ const DEFAULT_SOURCES = [
35
+ "ROUTE",
36
+ "QUERY",
37
+ "QUERY_ITEM"
38
+ ];
39
+ const coercionPipe = (opts) => {
40
+ const sources = new Set(opts?.sources ?? DEFAULT_SOURCES);
41
+ return (0, moost.definePipeFn)((value, metas) => {
42
+ const source = metas?.targetMeta?.paramSource;
43
+ if (!source || !sources.has(source)) return value;
44
+ const t = metas?.targetMeta?.type;
45
+ if ((0, __atscript_typescript_utils.isAnnotatedType)(t)) return (0, __atscript_typescript_utils.coerceForType)(t, value);
46
+ if (t === Number) return (0, __atscript_typescript_utils.coerceScalar)("number", value);
47
+ if (t === Boolean) return (0, __atscript_typescript_utils.coerceScalar)("boolean", value);
48
+ if (t === Date) return coerceDate(value);
49
+ return value;
50
+ }, moost.TPipePriority.TRANSFORM);
51
+ };
52
+ const UseCoercionPipe = (opts) => (0, moost.Pipe)(coercionPipe(opts));
53
+ function coerceDate(value) {
54
+ if (typeof value !== "string" || value.trim().length === 0) return value;
55
+ const parsed = new Date(value);
56
+ return Number.isNaN(parsed.getTime()) ? value : parsed;
57
+ }
58
+
59
+ //#endregion
29
60
  //#region packages/moost-validator/src/as-validator.pipe.ts
30
61
  const validatorPipe = (opts) => (0, moost.definePipeFn)((value, metas, level) => {
31
62
  if (metas?.targetMeta?.optional && (value === undefined || value === null)) return value;
@@ -56,7 +87,9 @@ const validationErrorTransform = () => (0, moost.defineInterceptor)({
56
87
  const UseValidationErrorTransform = () => (0, moost.Intercept)(validationErrorTransform());
57
88
 
58
89
  //#endregion
90
+ exports.UseCoercionPipe = UseCoercionPipe
59
91
  exports.UseValidationErrorTransform = UseValidationErrorTransform
60
92
  exports.UseValidatorPipe = UseValidatorPipe
93
+ exports.coercionPipe = coercionPipe
61
94
  exports.validationErrorTransform = validationErrorTransform
62
95
  exports.validatorPipe = validatorPipe
package/dist/index.d.ts CHANGED
@@ -1,6 +1,63 @@
1
1
  import * as moost from 'moost';
2
2
  import { TValidatorOptions } from '@atscript/typescript/utils';
3
3
 
4
+ /** Options for configuring {@link coercionPipe} behavior. */
5
+ interface TCoercionOptions {
6
+ /**
7
+ * Param sources (`paramSource` metadata stamped by Moost resolvers) the
8
+ * pipe coerces. Defaults to `['ROUTE', 'QUERY', 'QUERY_ITEM']` — the
9
+ * string-transport sources. Add `'BODY'` to opt in to body coercion.
10
+ */
11
+ sources?: string[];
12
+ }
13
+ /**
14
+ * **coercionPipe** ─ Creates a Moost *pipe* that coerces string-transport
15
+ * input (route params, query strings) toward the parameter's declared type
16
+ * before validation runs.
17
+ *
18
+ * For atscript-annotated types it delegates to `coerceForType` from
19
+ * `@atscript/typescript/utils` (scalars, unions, `@Query()` DTOs, arrays).
20
+ * For plain design types (`offset: number`, `flag: boolean`, `since: Date`)
21
+ * it falls back to direct constructor-based coercion — this also covers
22
+ * scalar `.as` aliases under tsc's `emitDecoratorMetadata`, where the alias
23
+ * identity collapses to `Number`.
24
+ *
25
+ * Coercion never throws and never validates — unparsable input passes
26
+ * through unchanged for {@link validatorPipe} to report. The pipe is
27
+ * registered at {@link TPipePriority.TRANSFORM}, so it composes ahead of
28
+ * `validatorPipe` (VALIDATE):
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const app = new Moost()
33
+ * app.applyGlobalPipes(coercionPipe(), validatorPipe())
34
+ *
35
+ * // KafkaOffset: @expect.int @expect.min 0 → export type KafkaOffset = number
36
+ * ‎@Get('topics/:topic')
37
+ * read(@Param('offset') offset: KafkaOffset) {
38
+ * // offset is a validated number — no manual Number.parseInt
39
+ * }
40
+ * ```
41
+ *
42
+ * @param opts {@link TCoercionOptions}.
43
+ * @returns A ready‑to‑use `PipeFn` instance.
44
+ */
45
+ declare const coercionPipe: (opts?: TCoercionOptions) => moost.TPipeFn<any>;
46
+ /**
47
+ * Syntactic sugar decorator that applies {@link coercionPipe} to a handler or
48
+ * an entire controller class.
49
+ *
50
+ * @param opts {@link TCoercionOptions}.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * ‎@Get('items')
55
+ * ‎@UseCoercionPipe()
56
+ * list(@Query() query: SearchQuery) {}
57
+ * ```
58
+ */
59
+ declare const UseCoercionPipe: (opts?: TCoercionOptions) => ClassDecorator & MethodDecorator & ParameterDecorator;
60
+
4
61
  /**
5
62
  * **validatorPipe** ─ Creates a Moost *pipe* that runs atscript validation on
6
63
  * handler parameters (body, params, query, etc.).
@@ -71,4 +128,5 @@ declare const validationErrorTransform: () => moost.TInterceptorDef;
71
128
  */
72
129
  declare const UseValidationErrorTransform: () => ClassDecorator & MethodDecorator;
73
130
 
74
- export { UseValidationErrorTransform, UseValidatorPipe, validationErrorTransform, validatorPipe };
131
+ export { UseCoercionPipe, UseValidationErrorTransform, UseValidatorPipe, coercionPipe, validationErrorTransform, validatorPipe };
132
+ export type { TCoercionOptions };
package/dist/index.mjs CHANGED
@@ -1,7 +1,38 @@
1
- import { ValidatorError, isAnnotatedType } from "@atscript/typescript/utils";
1
+ import { ValidatorError, coerceForType, coerceScalar, isAnnotatedType } from "@atscript/typescript/utils";
2
2
  import { Intercept, Pipe, TInterceptorPriority, TPipePriority, defineInterceptor, definePipeFn } from "moost";
3
3
  import { HttpError } from "@moostjs/event-http";
4
4
 
5
+ //#region packages/moost-validator/src/as-coercion.pipe.ts
6
+ /**
7
+ * Param sources that arrive as strings over the wire and are safe to coerce
8
+ * by default. `BODY` is deliberately excluded — coercing JSON bodies silently
9
+ * loosens the API contract; opt in via {@link TCoercionOptions.sources}.
10
+ */ const DEFAULT_SOURCES = [
11
+ "ROUTE",
12
+ "QUERY",
13
+ "QUERY_ITEM"
14
+ ];
15
+ const coercionPipe = (opts) => {
16
+ const sources = new Set(opts?.sources ?? DEFAULT_SOURCES);
17
+ return definePipeFn((value, metas) => {
18
+ const source = metas?.targetMeta?.paramSource;
19
+ if (!source || !sources.has(source)) return value;
20
+ const t = metas?.targetMeta?.type;
21
+ if (isAnnotatedType(t)) return coerceForType(t, value);
22
+ if (t === Number) return coerceScalar("number", value);
23
+ if (t === Boolean) return coerceScalar("boolean", value);
24
+ if (t === Date) return coerceDate(value);
25
+ return value;
26
+ }, TPipePriority.TRANSFORM);
27
+ };
28
+ const UseCoercionPipe = (opts) => Pipe(coercionPipe(opts));
29
+ function coerceDate(value) {
30
+ if (typeof value !== "string" || value.trim().length === 0) return value;
31
+ const parsed = new Date(value);
32
+ return Number.isNaN(parsed.getTime()) ? value : parsed;
33
+ }
34
+
35
+ //#endregion
5
36
  //#region packages/moost-validator/src/as-validator.pipe.ts
6
37
  const validatorPipe = (opts) => definePipeFn((value, metas, level) => {
7
38
  if (metas?.targetMeta?.optional && (value === undefined || value === null)) return value;
@@ -32,4 +63,4 @@ const validationErrorTransform = () => defineInterceptor({
32
63
  const UseValidationErrorTransform = () => Intercept(validationErrorTransform());
33
64
 
34
65
  //#endregion
35
- export { UseValidationErrorTransform, UseValidatorPipe, validationErrorTransform, validatorPipe };
66
+ export { UseCoercionPipe, UseValidationErrorTransform, UseValidatorPipe, coercionPipe, validationErrorTransform, validatorPipe };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-validator",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Validator pipe and utils for Moost.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -36,15 +36,15 @@
36
36
  },
37
37
  "dependencies": {},
38
38
  "devDependencies": {
39
- "@moostjs/event-http": "^0.6.31",
40
- "moost": "^0.6.31",
39
+ "@moostjs/event-http": "^0.6.33",
40
+ "moost": "^0.6.33",
41
41
  "vitest": "3.2.4"
42
42
  },
43
43
  "peerDependencies": {
44
- "@moostjs/event-http": "^0.6.31",
45
- "moost": "^0.6.31",
46
- "@atscript/core": "^0.1.83",
47
- "@atscript/typescript": "^0.1.83"
44
+ "@moostjs/event-http": "^0.6.33",
45
+ "moost": "^0.6.33",
46
+ "@atscript/core": "^0.1.85",
47
+ "@atscript/typescript": "^0.1.85"
48
48
  },
49
49
  "scripts": {
50
50
  "pub": "pnpm publish --access public",