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
JavaScript
;
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