najm-validation 0.1.1

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.
@@ -0,0 +1,201 @@
1
+ import * as diject from 'diject';
2
+ import * as najm_core from 'najm-core';
3
+
4
+ /**
5
+ * Validation target type
6
+ */
7
+ type ValidationTarget = 'body' | 'params' | 'query' | 'headers';
8
+ /**
9
+ * Minimal schema shape required by validation plugin.
10
+ * Supports both Zod v3 and v4 schema instances.
11
+ */
12
+ interface ValidationSchema {
13
+ parse(data: unknown): unknown;
14
+ strip?: () => ValidationSchema;
15
+ }
16
+ /**
17
+ * Minimal Zod issue shape used for error formatting.
18
+ */
19
+ interface ZodIssueLike {
20
+ path: PropertyKey[];
21
+ message: string;
22
+ code: string;
23
+ }
24
+ /**
25
+ * Minimal Zod error shape used by framework internals.
26
+ * Compatible with Zod v3 and v4 errors.
27
+ */
28
+ interface ZodErrorLike {
29
+ issues: ZodIssueLike[];
30
+ }
31
+ /**
32
+ * Error formatter function type
33
+ */
34
+ type ErrorFormatter = (error: ZodErrorLike, target: ValidationTarget) => any;
35
+ /**
36
+ * Validation configuration for @Validate decorator
37
+ */
38
+ interface ValidationConfig {
39
+ /**
40
+ * Zod schema for request body validation
41
+ */
42
+ body?: ValidationSchema;
43
+ /**
44
+ * Zod schema for route params validation
45
+ */
46
+ params?: ValidationSchema;
47
+ /**
48
+ * Zod schema for query parameters validation
49
+ */
50
+ query?: ValidationSchema;
51
+ /**
52
+ * Zod schema for request headers validation
53
+ */
54
+ headers?: ValidationSchema;
55
+ /**
56
+ * Remove unknown fields from validated data (default: false)
57
+ */
58
+ stripUnknown?: boolean;
59
+ /**
60
+ * HTTP status code for validation errors (default: 400)
61
+ */
62
+ errorStatus?: number;
63
+ /**
64
+ * Custom error formatter for validation errors
65
+ */
66
+ errorFormatter?: ErrorFormatter;
67
+ }
68
+ /**
69
+ * Input type for @Validate decorator
70
+ * Can be a Zod schema (assumes body validation) or full config object
71
+ */
72
+ type ValidateInput = ValidationSchema | ValidationConfig;
73
+ /**
74
+ * Plugin configuration options
75
+ */
76
+ interface ValidationPluginConfig {
77
+ /**
78
+ * Enable or disable validation globally (default: true)
79
+ */
80
+ enabled?: boolean;
81
+ /**
82
+ * Default strip unknown fields behavior (default: false)
83
+ */
84
+ stripUnknown?: boolean;
85
+ /**
86
+ * Default HTTP status code for validation errors (default: 400)
87
+ */
88
+ errorStatus?: number;
89
+ /**
90
+ * Global error formatter
91
+ */
92
+ errorFormatter?: ErrorFormatter;
93
+ }
94
+
95
+ /**
96
+ * Metadata key for @Validate decorator
97
+ */
98
+ declare const VALIDATE_META: unique symbol;
99
+ /**
100
+ * Configuration token for validation plugin
101
+ */
102
+ declare const VALIDATION_CONFIG: unique symbol;
103
+ /**
104
+ * ALS tokens for validated request data
105
+ * Stored in AsyncLocalStorage for request-scoped access
106
+ */
107
+ declare const VALIDATED_BODY: diject.AlsToken<any>;
108
+ declare const VALIDATED_PARAMS: diject.AlsToken<any>;
109
+ declare const VALIDATED_QUERY: diject.AlsToken<any>;
110
+ declare const VALIDATED_HEADERS: diject.AlsToken<any>;
111
+
112
+ /**
113
+ * @Validate decorator - Validates request data using Zod schemas
114
+ *
115
+ * Smart defaults: Pass a schema directly for body validation (most common use case)
116
+ *
117
+ * @example
118
+ * // Body validation (90% use case)
119
+ * @Validate(CreateUserSchema)
120
+ * createUser(@Body() data) { ... }
121
+ *
122
+ * @example
123
+ * // Multiple targets
124
+ * @Validate({
125
+ * body: CreateUserSchema,
126
+ * params: z.object({ id: z.string().uuid() })
127
+ * })
128
+ * updateUser(@Params('id') id, @Body() data) { ... }
129
+ *
130
+ * @example
131
+ * // With options
132
+ * @Validate({
133
+ * body: CreateUserSchema,
134
+ * stripUnknown: true,
135
+ * errorStatus: 422
136
+ * })
137
+ * createUser(@Body() data) { ... }
138
+ */
139
+ declare function Validate(input: ValidateInput): MethodDecorator;
140
+ /**
141
+ * Helper to retrieve validation config from metadata
142
+ */
143
+ declare function getValidationConfig(target: any, methodName?: string | symbol): ValidationConfig | undefined;
144
+
145
+ /**
146
+ * Validation plugin factory
147
+ *
148
+ * Provides request validation using Zod schemas with smart defaults
149
+ *
150
+ * @example
151
+ * // Default configuration
152
+ * const server = new Server()
153
+ * .use(validation())
154
+ *
155
+ * @example
156
+ * // Custom configuration
157
+ * const server = new Server()
158
+ * .use(validation({
159
+ * stripUnknown: true,
160
+ * errorStatus: 422,
161
+ * errorFormatter: customFormatter
162
+ * }))
163
+ *
164
+ * @example
165
+ * // Disable validation
166
+ * const server = new Server()
167
+ * .use(validation({ enabled: false }))
168
+ */
169
+ declare const validation: (config?: ValidationPluginConfig) => najm_core.NajmPlugin;
170
+
171
+ declare class ValidationService {
172
+ private container;
173
+ private scanner;
174
+ private config;
175
+ private log;
176
+ private validationCount;
177
+ private defaultErrorStatus;
178
+ private defaultStripUnknown;
179
+ private defaultErrorFormatter?;
180
+ scan(): Promise<void>;
181
+ configure(): Promise<void>;
182
+ activate(): Promise<void>;
183
+ onReady(): Promise<void>;
184
+ /**
185
+ * Create validation middleware for a specific route
186
+ * Note: ctx param required by Hono but we use ALS internally
187
+ */
188
+ private createValidationMiddleware;
189
+ /**
190
+ * Validate a specific request target using ALS
191
+ */
192
+ private validateTarget;
193
+ private isZodErrorLike;
194
+ /**
195
+ * Extract data from ALS instead of ctx
196
+ */
197
+ private extractData;
198
+ private throwValidationError;
199
+ }
200
+
201
+ export { type ErrorFormatter, VALIDATED_BODY, VALIDATED_HEADERS, VALIDATED_PARAMS, VALIDATED_QUERY, VALIDATE_META, VALIDATION_CONFIG, Validate, type ValidateInput, type ValidationConfig, type ValidationPluginConfig, type ValidationSchema, ValidationService, type ValidationTarget, type ZodErrorLike, type ZodIssueLike, getValidationConfig, validation };
package/dist/index.js ADDED
@@ -0,0 +1,221 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/tokens.ts
5
+ import { createAlsToken } from "najm-core";
6
+ var VALIDATE_META = /* @__PURE__ */ Symbol("validate");
7
+ var VALIDATION_CONFIG = /* @__PURE__ */ Symbol("validation:config");
8
+ var VALIDATED_BODY = createAlsToken("validated:body");
9
+ var VALIDATED_PARAMS = createAlsToken("validated:params");
10
+ var VALIDATED_QUERY = createAlsToken("validated:query");
11
+ var VALIDATED_HEADERS = createAlsToken("validated:headers");
12
+
13
+ // src/decorator.ts
14
+ import { MetaHelper } from "najm-core";
15
+ function isValidationSchema(value) {
16
+ return value !== null && value !== void 0 && typeof value === "object" && "parse" in value && typeof value.parse === "function";
17
+ }
18
+ __name(isValidationSchema, "isValidationSchema");
19
+ function Validate(input) {
20
+ const config = isValidationSchema(input) ? { body: input } : input;
21
+ return (target, methodName) => {
22
+ MetaHelper.define(VALIDATE_META, config, target[methodName]);
23
+ };
24
+ }
25
+ __name(Validate, "Validate");
26
+ function getValidationConfig(target, methodName) {
27
+ if (methodName) {
28
+ return MetaHelper.get(VALIDATE_META, target[methodName]);
29
+ }
30
+ return MetaHelper.get(VALIDATE_META, target);
31
+ }
32
+ __name(getValidationConfig, "getValidationConfig");
33
+
34
+ // src/ValidationPlugin.ts
35
+ import { plugin } from "najm-core";
36
+
37
+ // src/ValidationService.ts
38
+ import { LoggerService, ScannerService, Scan, ScanType, INJECTION_TYPES, Err } from "najm-core";
39
+ import { Container, DI, Inject, Meta, Service } from "najm-core";
40
+ import { CONTEXT, REQUEST, PARSER } from "najm-core";
41
+ var __decorate = function(decorators, target, key, desc) {
42
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
43
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
44
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
45
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
46
+ };
47
+ var __metadata = function(k, v) {
48
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
49
+ };
50
+ var _a;
51
+ var _b;
52
+ var _c;
53
+ var ValidationService = class ValidationService2 {
54
+ static {
55
+ __name(this, "ValidationService");
56
+ }
57
+ container;
58
+ scanner;
59
+ config;
60
+ log;
61
+ validationCount = 0;
62
+ defaultErrorStatus = 400;
63
+ defaultStripUnknown = false;
64
+ defaultErrorFormatter;
65
+ async scan() {
66
+ this.validationCount = 0;
67
+ if (this.config?.enabled === false) {
68
+ this.log.info("Validation plugin disabled");
69
+ return;
70
+ }
71
+ this.scanner.scan(ScanType.CONTROLLER, {
72
+ onMethod: /* @__PURE__ */ __name((controller, methodName) => {
73
+ const validationConfig = getValidationConfig(controller.prototype, methodName);
74
+ if (validationConfig) {
75
+ this.container.setInjection({
76
+ type: INJECTION_TYPES.MIDDLEWARE,
77
+ target: controller,
78
+ methodName,
79
+ handler: this.createValidationMiddleware(validationConfig),
80
+ order: 45,
81
+ source: "validation"
82
+ });
83
+ this.validationCount++;
84
+ }
85
+ }, "onMethod")
86
+ });
87
+ }
88
+ async configure() {
89
+ if (this.config) {
90
+ this.defaultErrorStatus = this.config.errorStatus ?? 400;
91
+ this.defaultStripUnknown = this.config.stripUnknown ?? false;
92
+ this.defaultErrorFormatter = this.config.errorFormatter;
93
+ }
94
+ }
95
+ async activate() {
96
+ }
97
+ async onReady() {
98
+ if (this.validationCount > 0) {
99
+ this.log.info(`Validation: ${this.validationCount} route(s) configured`);
100
+ }
101
+ }
102
+ /**
103
+ * Create validation middleware for a specific route
104
+ * Note: ctx param required by Hono but we use ALS internally
105
+ */
106
+ createValidationMiddleware(config) {
107
+ return async (_ctx, next) => {
108
+ const targets = ["body", "params", "query", "headers"];
109
+ for (const target of targets) {
110
+ const schema = config[target];
111
+ if (schema) {
112
+ await this.validateTarget(target, schema, config);
113
+ }
114
+ }
115
+ return next();
116
+ };
117
+ }
118
+ /**
119
+ * Validate a specific request target using ALS
120
+ */
121
+ async validateTarget(target, schema, config) {
122
+ const data = await this.extractData(target);
123
+ const shouldStrip = config.stripUnknown ?? this.defaultStripUnknown;
124
+ try {
125
+ const finalSchema = shouldStrip && typeof schema.strip === "function" ? schema.strip() : schema;
126
+ const validatedData = finalSchema.parse(data);
127
+ const tokenMap = {
128
+ body: VALIDATED_BODY,
129
+ params: VALIDATED_PARAMS,
130
+ query: VALIDATED_QUERY,
131
+ headers: VALIDATED_HEADERS
132
+ };
133
+ this.container.set(tokenMap[target], validatedData);
134
+ } catch (error) {
135
+ if (this.isZodErrorLike(error)) {
136
+ this.throwValidationError(error, target, config);
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+ isZodErrorLike(error) {
142
+ if (!error || typeof error !== "object")
143
+ return false;
144
+ const issues = error.issues;
145
+ return Array.isArray(issues);
146
+ }
147
+ /**
148
+ * Extract data from ALS instead of ctx
149
+ */
150
+ async extractData(target) {
151
+ const request = this.container.get(REQUEST);
152
+ const parser = this.container.get(PARSER);
153
+ const context = this.container.get(CONTEXT);
154
+ switch (target) {
155
+ case "body":
156
+ return parser.parseBody();
157
+ case "params":
158
+ return context.req.param();
159
+ // or request.params if HRequest has it parsed
160
+ case "query":
161
+ return request.query;
162
+ case "headers":
163
+ return request.headers;
164
+ default:
165
+ return {};
166
+ }
167
+ }
168
+ throwValidationError(error, target, config) {
169
+ const errorStatus = config.errorStatus ?? this.defaultErrorStatus;
170
+ const formatter = config.errorFormatter ?? this.defaultErrorFormatter;
171
+ if (formatter) {
172
+ try {
173
+ const customResponse = formatter(error, target);
174
+ const customError = Err.createFromZod(error, target, errorStatus);
175
+ customError.toJSON = () => customResponse;
176
+ throw customError;
177
+ } catch (formatterError) {
178
+ if (formatterError instanceof Error && formatterError.message.includes("Cannot read")) {
179
+ Err.fromZod(error, target, errorStatus);
180
+ }
181
+ throw formatterError;
182
+ }
183
+ }
184
+ Err.fromZod(error, target, errorStatus);
185
+ }
186
+ };
187
+ __decorate([
188
+ DI(),
189
+ __metadata("design:type", typeof (_a = typeof Container !== "undefined" && Container) === "function" ? _a : Object)
190
+ ], ValidationService.prototype, "container", void 0);
191
+ __decorate([
192
+ Scan(),
193
+ __metadata("design:type", typeof (_b = typeof ScannerService !== "undefined" && ScannerService) === "function" ? _b : Object)
194
+ ], ValidationService.prototype, "scanner", void 0);
195
+ __decorate([
196
+ Inject(VALIDATION_CONFIG),
197
+ __metadata("design:type", Object)
198
+ ], ValidationService.prototype, "config", void 0);
199
+ __decorate([
200
+ Inject(LoggerService),
201
+ __metadata("design:type", typeof (_c = typeof LoggerService !== "undefined" && LoggerService) === "function" ? _c : Object)
202
+ ], ValidationService.prototype, "log", void 0);
203
+ ValidationService = __decorate([
204
+ Service(),
205
+ Meta({ layer: "plugin", order: 45 })
206
+ ], ValidationService);
207
+
208
+ // src/ValidationPlugin.ts
209
+ var validation = /* @__PURE__ */ __name((config = {}) => plugin("validation").version("0.0.1").services(ValidationService).config(VALIDATION_CONFIG, config).build(), "validation");
210
+ export {
211
+ VALIDATED_BODY,
212
+ VALIDATED_HEADERS,
213
+ VALIDATED_PARAMS,
214
+ VALIDATED_QUERY,
215
+ VALIDATE_META,
216
+ VALIDATION_CONFIG,
217
+ Validate,
218
+ ValidationService,
219
+ getValidationConfig,
220
+ validation
221
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "najm-validation",
3
+ "version": "0.1.1",
4
+ "description": "Request validation plugin for Najm framework using Zod schemas",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "bun": "./src/index.ts",
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./*": {
19
+ "bun": "./src/*.ts",
20
+ "types": "./src/*.ts",
21
+ "import": "./src/*.ts",
22
+ "default": "./src/*.ts"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "test": "bun test",
28
+ "test:watch": "bun test --watch",
29
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
30
+ "typecheck:test": "tsc -p tsconfig.test.json"
31
+ },
32
+ "dependencies": {
33
+ "najm-core": "^0.1.1"
34
+ },
35
+ "peerDependencies": {
36
+ "hono": "^4.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "^5.7.3",
40
+ "rimraf": "^6.0.1",
41
+ "tsup": "^8.0.0"
42
+ }
43
+ }