import { apply } from "@decaf-ts/decoration";
import { ApiExcludeEndpoint } from "@nestjs/swagger";
import {
BulkCrudOperationKeys,
OperationKeys,
CrudOperations,
} from "@decaf-ts/db-decorators";
import { ModelConstructor } from "@decaf-ts/decorator-validation";
import { Delete, Get, Patch, Post, Put } from "@nestjs/common";
import { isOperationBlocked as coreIsOperationBlocked } from "@decaf-ts/core";
import { PreparedStatementKeys } from "@decaf-ts/core";
import { HttpVerbs } from "./types";
/**
* @description Determines if a given CRUD operation is blocked for a specific model constructor.
* @summary Retrieves the operation-blocking handler metadata stored on the provided model constructor (under `OperationKeys.REFLECT + OperationKeys.BLOCK`), executes it (if present) with its persisted arguments plus the requested operation, and returns whether the operation is blocked. If no handler exists, the operation is considered allowed (returns `false`).
* @param {ModelConstructor<any>} ModelConstructor - The target model constructor whose metadata may include a blocking handler.
* @param {CrudOperations} op - The CRUD operation to evaluate (e.g., `OperationKeys.CREATE`, `OperationKeys.READ`, `OperationKeys.UPDATE`, `OperationKeys.DELETE`).
* @return {boolean} `true` when the operation is explicitly blocked by the model's handler; otherwise `false`.
* @function isOperationBlocked
*/
/**
* @description Conditionally applies an HTTP method decorator for a given model and verb, hiding the endpoint in Swagger (and not registering the route) when the model blocks that CRUD operation.
* @summary Maps an HTTP verb to its corresponding `CrudOperations` key and Nest HTTP decorator (`@Get`, `@Post`, etc.). It checks `isOperationBlocked(ModelConstructor, crudOp)` and, if blocked, applies only `@ApiExcludeEndpoint()` (Swagger-hidden, no Nest route). If permitted, it applies the appropriate HTTP decorator with the optional `path`.
* @param {ModelConstructor<any>} ModelConstructor - The model constructor used to resolve operation-blocking rules.
* @param {HttpVerbs} verb - The HTTP verb to map (e.g., `"GET" | "POST" | "PUT" | "PATCH" | "DELETE"`).
* @param {string} [path] - Optional route path passed through to the corresponding Nest HTTP method decorator.
* @return {MethodDecorator} A method decorator that either excludes the endpoint from Swagger (and route registration) or applies the correct HTTP decorator.
*/
export function ApiOperationFromModel(
ModelConstructor: ModelConstructor<any>,
verb: HttpVerbs,
path?: string
): MethodDecorator {
const httpToCrud: Record<
HttpVerbs,
[CrudOperations, (path?: string) => MethodDecorator]
> = {
GET: [OperationKeys.READ, Get],
POST: [OperationKeys.CREATE, Post],
PUT: [OperationKeys.UPDATE, Put],
PATCH: [OperationKeys.UPDATE, Patch],
DELETE: [OperationKeys.DELETE, Delete],
};
const [crudOp, HttpMethodDecorator] = httpToCrud[verb];
const target = resolveBlockTarget(verb, path);
return target
? coreIsOperationBlocked(ModelConstructor, target.kind as any, target.value)
? apply(ApiExcludeEndpoint())
: apply(HttpMethodDecorator(path))
: coreIsOperationBlocked(ModelConstructor, crudOp)
? apply(ApiExcludeEndpoint())
: apply(HttpMethodDecorator(path));
}
/**
* @description Conditionally applies an HTTP method decorator for a given model and verb, hiding the endpoint in Swagger (and not registering the route) when the model blocks that CRUD operation.
* @summary Maps an HTTP verb to its corresponding `CrudOperations` key and Nest HTTP decorator (`@Get`, `@Post`, etc.). It checks `isOperationBlocked(ModelConstructor, crudOp)` and, if blocked, applies only `@ApiExcludeEndpoint()` (Swagger-hidden, no Nest route). If permitted, it applies the appropriate HTTP decorator with the optional `path`.
* @param {ModelConstructor<any>} ModelConstructor - The model constructor used to resolve operation-blocking rules.
* @param {HttpVerbs} verb - The HTTP verb to map (e.g., `"GET" | "POST" | "PUT" | "PATCH" | "DELETE"`).
* @param {string} [path] - Optional route path passed through to the corresponding Nest HTTP method decorator.
* @return {MethodDecorator} A method decorator that either excludes the endpoint from Swagger (and route registration) or applies the correct HTTP decorator.
*/
export function BulkApiOperationFromModel(
ModelConstructor: ModelConstructor<any>,
verb: HttpVerbs,
path?: string
): MethodDecorator {
const httpToCrud: Record<
HttpVerbs,
[BulkCrudOperationKeys, (path?: string) => MethodDecorator]
> = {
GET: [BulkCrudOperationKeys.READ_ALL, Get],
POST: [BulkCrudOperationKeys.CREATE_ALL, Post],
PUT: [BulkCrudOperationKeys.UPDATE_ALL, Put],
PATCH: [BulkCrudOperationKeys.UPDATE_ALL, Patch],
DELETE: [BulkCrudOperationKeys.DELETE_ALL, Delete],
};
const [crudOp, HttpMethodDecorator] = httpToCrud[verb];
const target = path ? resolveBlockTarget(verb, path) : undefined;
return target
? coreIsOperationBlocked(ModelConstructor, target.kind as any, target.value)
? apply(ApiExcludeEndpoint())
: apply(HttpMethodDecorator(path))
: coreIsOperationBlocked(ModelConstructor, "bulk" as any, crudOp as any)
? apply(ApiExcludeEndpoint())
: apply(HttpMethodDecorator(path));
}
function resolveBlockTarget(
verb: HttpVerbs,
path?: string
):
| { kind: "crud" | "statement" | "query" | "bulk"; value: string }
| undefined {
if (!path) return undefined;
const normalized = path.replace(/^\/+|\/+$/g, "");
const statementTargets: Record<string, string> = {
"listBy/:key": PreparedStatementKeys.LIST_BY,
"paginateBy/:key/:page": PreparedStatementKeys.PAGE_BY,
"find/:value": PreparedStatementKeys.FIND,
"page/:value": PreparedStatementKeys.PAGE,
"findOneBy/:key/:value": PreparedStatementKeys.FIND_ONE_BY,
"findBy/:key/:value": PreparedStatementKeys.FIND_BY,
"statement/:method/*args": "statement",
"countOf/:field": PreparedStatementKeys.COUNT_OF,
"maxOf/:field": PreparedStatementKeys.MAX_OF,
"minOf/:field": PreparedStatementKeys.MIN_OF,
"avgOf/:field": PreparedStatementKeys.AVG_OF,
"sumOf/:field": PreparedStatementKeys.SUM_OF,
"distinctOf/:field": PreparedStatementKeys.DISTINCT_OF,
"groupOf/:field": PreparedStatementKeys.GROUP_OF,
};
if (normalized.startsWith("query/")) {
return { kind: "query", value: normalized.replace(/^query\//, "") };
}
const statementValue = statementTargets[normalized];
if (statementValue) {
return { kind: "statement", value: statementValue };
}
if (verb === "GET" && normalized === "") {
return { kind: "crud", value: OperationKeys.READ };
}
return undefined;
}
Source