UNPKG

tspace-spear

Version:

tspace-spear is a lightweight, high-performance API framework for Node.js that leverages the native HTTP server and supports uWebSockets.js (C++) for maximum speed and efficiency.

340 lines 10.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Validate = exports.Cookies = exports.Query = exports.Params = exports.Files = exports.Body = void 0; exports.createDtoDecorator = createDtoDecorator; exports.createContextDecorator = createContextDecorator; exports.ValidateDto = ValidateDto; const package_1 = __importDefault(require("../package")); class ValidateError extends Error { issues; status; constructor(message, { issues = [], status = 400 } = {}) { super(message); this.name = "ValidateError"; this.issues = issues; this.status = status; Object.setPrototypeOf(this, ValidateError.prototype); } } function createDtoDecorator(validator, onError) { return (_target, _key, descriptor) => { const original = descriptor.value; descriptor.value = async function (ctx, next) { try { await validator(ctx); return await original.call(this, ctx, next); } catch (err) { if (onError) { return onError(ctx, err); } let message = err?.message ?? "Bad Request"; let issues = err.issues ?? err.errors ?? []; let status = +(err.status ?? 400); if (!Number.isInteger(status) || status < 100 || status > 599) { status = 400; } if (err.name === "ZodError") { const zodIssues = err.issues .map((i) => ({ path: i.path.join("."), message: i.message, })); message = "Validation failed"; issues = zodIssues; status = 422; } return ctx.res .status(status) .json({ message, issues, }); } }; return descriptor; }; } function createContextDecorator(hook) { return (target, key, descriptor) => { const original = descriptor.value; descriptor.value = async function (ctx, next) { return await hook(ctx, async () => { return await original.call(this, ctx, next); }, { target, key, descriptor, }); }; return descriptor; }; } /** * Extract specific fields from `ctx.body`. * * This decorator filters the request body and keeps only the * specified keys. The filtered result replaces `ctx.body` * before the controller method is executed. * * @example * ```ts * @Body('email', 'password') * async login(ctx: T.Context) { * // ctx.body = { email: "...", password: "..." } * } * ``` * * @param {...string} keys - Body field names to extract. * @returns {MethodDecorator} */ const Body = (...keys) => { return createContextDecorator(async (ctx, next) => { const body = ctx.body; ctx.body = keys.reduce((prev, curr) => (body[curr] != null ? { ...prev, [curr]: body[curr] } : prev), {}); return await next(); }); }; exports.Body = Body; /** * Extract specific uploaded files from `ctx.files`. * * Filters uploaded files and keeps only the specified * file field names before executing the controller method. * * @example * ```ts * \@Files('avatar', 'resume') * async upload(ctx: T.Context) { * // ctx.files = { avatar: File, resume: File } * } * ``` * * @param {...string} keys - File field names to extract. * @returns {MethodDecorator} */ const Files = (...keys) => { return createContextDecorator(async (ctx, next) => { const files = ctx.files; ctx.files = keys.reduce((prev, curr) => (files[curr] != null ? { ...prev, [curr]: files[curr] } : prev), {}); return await next(); }); }; exports.Files = Files; /** * Extract specific route parameters from `ctx.params`. * * Filters route parameters and keeps only the specified * parameter names. * * @example * ```ts * \@Params('id') * async getUser(ctx: T.Context) { * // ctx.params = { id: "123" } * } * ``` * * @param {...string} keys - Route parameter names to extract. * @returns {MethodDecorator} */ const Params = (...keys) => { return createContextDecorator(async (ctx, next) => { const params = ctx.params; ctx.params = keys.reduce((prev, curr) => (params[curr] != null ? { ...prev, [curr]: params[curr] } : prev), {}); return await next(); }); }; exports.Params = Params; /** * Extract specific query parameters from `ctx.query`. * * Filters query parameters and keeps only the specified * query keys. * * @example * ```ts * \@Query('page', 'limit') * async listUsers(ctx: T.Context) { * // ctx.query = { page: "1", limit: "10" } * } * ``` * * @param {...string} keys - Query parameter names to extract. * @returns {MethodDecorator} */ const Query = (...keys) => { return createContextDecorator(async (ctx, next) => { const query = ctx.query; ctx.query = keys.reduce((prev, curr) => (query[curr] != null ? { ...prev, [curr]: query[curr] } : prev), {}); return await next(); }); }; exports.Query = Query; /** * Extract specific cookies from `ctx.cookies`. * * Filters cookies and keeps only the specified cookie keys * before executing the controller method. * * @example * ```ts * \@Cookies('token') * async profile(ctx: T.Context) { * // ctx.cookies = { token: "..." } * } * ``` * * @param {...string} keys - Cookie names to extract. * @returns {MethodDecorator} */ const Cookies = (...keys) => { return createContextDecorator(async (ctx, next) => { const cookies = ctx.cookies; ctx.cookies = keys.reduce((prev, curr) => (cookies[curr] != null ? { ...prev, [curr]: cookies[curr] } : prev), {}); return await next(); }); }; exports.Cookies = Cookies; /** * Validates required fields from a request target. * * @param keys - List of required field names. * @param options - Validation options. * @property options.target - Request source to validate. defaults to `"body"`. * * @property options.required - Enables required-value validation. * * - `true` * - Rejects `null` * - Rejects empty strings (`""`) * * - `object` * - Allows customizing required rules. * * @property options.required.allowNull - Allow `null` values. Default: `false` * * @property options.required.allowEmptyString - Allow empty string values. Default: `false` * @returns {MethodDecorator} * * @throws {Object} Throws a validation error object when * one or more required fields are missing. * * @example * ```ts * \@Validate(["email", "password"], { * required: { * allowEmptyString : true, * allowNull : false * } * }); * ``` * * @example * ```ts * \@Validate(["id"], { target: "query" }); * ``` */ const Validate = (keys, { target, required } = {}) => { return createDtoDecorator((ctx) => { const payload = ctx[target ?? 'body'] ?? {}; const issues = []; const requiredOpts = typeof required === "object" ? required : {}; const allowNull = requiredOpts.allowNull ?? false; const allowEmptyString = requiredOpts.allowEmptyString ?? false; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = payload[key]; if (value === undefined) { issues.push({ path: key, message: "Missing field", }); continue; } if (required) { const isNull = value === null && !allowNull; const isEmptyString = typeof value === "string" && value.trim() === "" && !allowEmptyString; if (isNull || isEmptyString) { issues.push({ path: key, message: "Field is required", }); } } } if (issues.length) { throw { message: "Validation failed", issues }; } return; }); }; exports.Validate = Validate; function ValidateDto(schema, options) { const status = options?.status ?? 422; const message = options?.message ?? "Validation failed"; const adaptor = options?.adaptor ?? "class-validator"; const target = options?.target ?? "body"; return createDtoDecorator(async (ctx) => { if (adaptor === "zod") { const result = await schema.safeParseAsync(ctx[target]); if (result.success) { ctx[target] = result.data; return; } const errors = result.error?.issues; const issues = Object .values(errors.reduce((acc, issue) => { const key = issue.path.join("."); if (!acc[key]) { acc[key] = { path: key, constraints: { [issue.code]: issue.message }, message: issue.message }; } else { acc[key].constraints[issue.code] = issue.message; acc[key].message += `, ${issue.message}`; } return acc; }, {})); throw new ValidateError(message, { issues, status }); } if (adaptor === "class-validator") { const dto = package_1.default .classTransformer .plainToInstance(schema, ctx[target]); const errors = await package_1.default .classValidator .validate(dto); if (!errors.length) { ctx[target] = dto; return; } const issues = errors.flatMap((error) => { const constraints = error.constraints ?? {}; return { path: error.property, constraints, message: Object.values(constraints).join(","), }; }); throw new ValidateError(message, { issues, status }); } throw new Error("Invalid validation adaptor specified"); }); } //# sourceMappingURL=context.js.map