@elsikora/nestjs-crud-automator
Version:
A library for automating the creation of CRUD operations in NestJS.
249 lines (246 loc) • 13 kB
JavaScript
import { AsyncLocalStorage } from 'node:async_hooks';
import { ApiControllerGeneratedScopeWhereContract } from './scope-where-contract.class.js';
import { ApiControllerGeneratedSecuritySnapshot } from './security-snapshot.class.js';
import { AuthorizationScopeMergeWhere } from '../../../../utility/authorization/scope-merge-where.utility.js';
import { ErrorException } from '../../../../utility/error/exception.utility.js';
import { ObjectFindPropertyDescriptor } from '../../../../utility/object-find-property-descriptor.utility.js';
import { InstanceChecker, EqualOperator, FindOperator } from 'typeorm';
/**
* Preserves pagination-critical GET_MANY options owned by a generated cursor route.
* Direct service calls retain the ordinary subscriber contract.
*/
class ApiControllerGeneratedGetManyContract {
static STORAGE = new AsyncLocalStorage();
static createSnapshot(properties) {
const snapshot = {
order: properties.order,
take: properties.take,
};
for (const key of ["join", "lock", "withDeleted"]) {
this.copyDataProperty(properties, snapshot, key);
}
return ApiControllerGeneratedSecuritySnapshot.detach(snapshot);
}
static hasActiveSession() {
return this.STORAGE.getStore() !== undefined;
}
static protect(properties, snapshot) {
const session = this.STORAGE.getStore();
const protectedProperties = Object.create(Object.getPrototypeOf(properties));
for (const key of Reflect.ownKeys(properties)) {
if (key === "cache" || key === "join" || key === "lock" || key === "order" || key === "select" || key === "skip" || key === "take" || key === "withDeleted") {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(properties, key);
if (descriptor) {
Object.defineProperty(protectedProperties, key, descriptor);
}
}
for (const key of ["select", "skip"]) {
Object.defineProperty(protectedProperties, key, {
// eslint-disable-next-line @elsikora/typescript/naming-convention
configurable: true,
// eslint-disable-next-line @elsikora/typescript/naming-convention
enumerable: true,
value: undefined,
// eslint-disable-next-line @elsikora/typescript/naming-convention
writable: true,
});
}
this.defineValue(protectedProperties, "cache", false);
for (const key of ["order", "take"]) {
const descriptor = Object.getOwnPropertyDescriptor(snapshot, key);
if (descriptor) {
Object.defineProperty(protectedProperties, key, descriptor);
}
}
if (session) {
for (const key of ["join", "lock"]) {
this.defineValue(protectedProperties, key);
}
this.copyDataProperty(properties, protectedProperties, "withDeleted");
this.defineValue(protectedProperties, "where", this.resolveScopedWindowWhere(session));
}
else {
for (const key of ["join", "lock", "withDeleted"]) {
const descriptor = Object.getOwnPropertyDescriptor(snapshot, key);
if (descriptor) {
Object.defineProperty(protectedProperties, key, descriptor);
}
}
}
return protectedProperties;
}
static async resolveBefore(properties, callback) {
const session = this.STORAGE.getStore();
if (!session) {
return (await callback(properties)) ?? properties;
}
if (session.state.isPrepared) {
return this.applyCandidateProperties(properties, session.state.candidateProperties);
}
session.state.isPrepared = true;
const beforeProperties = ApiControllerGeneratedSecuritySnapshot.detach(session.baseProperties);
const subscriberProperties = ApiControllerGeneratedSecuritySnapshot.detach((await callback(beforeProperties)) ?? beforeProperties);
this.assertSupportedCandidateProperties(subscriberProperties);
session.state.candidateProperties = this.createCandidateProperties(subscriberProperties);
this.assertStableCandidateWhere(session.state.candidateProperties.where);
session.state.isCandidateWhereScoped = ApiControllerGeneratedScopeWhereContract.contains(session.state.candidateProperties.where, session.baseProperties.where);
return this.applyCandidateProperties(subscriberProperties, session.state.candidateProperties);
}
static run(baseProperties, callback) {
this.assertStableCandidateWhere(baseProperties.where);
return this.STORAGE.run({
baseProperties: ApiControllerGeneratedSecuritySnapshot.detach(baseProperties),
state: { isPrepared: false },
}, callback);
}
static runWindow(windowWhere, callback) {
const session = this.STORAGE.getStore();
if (!session) {
return callback();
}
return this.STORAGE.run({
...session,
windowWhere: ApiControllerGeneratedSecuritySnapshot.detach(windowWhere),
}, callback);
}
static applyCandidateProperties(properties, candidateProperties) {
const result = ApiControllerGeneratedSecuritySnapshot.detach(properties);
if (candidateProperties) {
for (const key of ["where", "withDeleted"]) {
const descriptor = Object.getOwnPropertyDescriptor(candidateProperties, key);
if (descriptor) {
Object.defineProperty(result, key, ApiControllerGeneratedSecuritySnapshot.detach(descriptor));
}
}
}
return result;
}
static assertCanonicalFindOperator(value, isEqualOperator) {
const expectedKeys = ["@instanceof", "_getSql", "_multipleParameters", "_objectLiteralParameters", "_type", "_useParameter", "_value"];
const actualKeys = Reflect.ownKeys(value);
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key) => typeof key !== "string" || !expectedKeys.includes(key))) {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains a non-canonical TypeORM operator");
}
for (const key of expectedKeys) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor || !("value" in descriptor) || !descriptor.configurable || !descriptor.enumerable || !descriptor.writable) {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains a non-canonical TypeORM operator");
}
}
const expectedMarker = Symbol.for(isEqualOperator ? "EqualOperator" : "FindOperator");
const marker = Object.getOwnPropertyDescriptor(value, "@instanceof")?.value;
const type = Object.getOwnPropertyDescriptor(value, "_type")?.value;
const getSql = Object.getOwnPropertyDescriptor(value, "_getSql")?.value;
const objectLiteralParameters = Object.getOwnPropertyDescriptor(value, "_objectLiteralParameters")?.value;
if (marker !== expectedMarker || typeof type !== "string") {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains a non-canonical TypeORM operator");
}
if (type === "raw") {
throw ErrorException("CURSOR GET_LIST candidate WHERE cannot contain raw SQL operators");
}
if (getSql !== undefined || objectLiteralParameters !== undefined) {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains a non-canonical TypeORM operator");
}
}
static assertStableCandidateWhere(value, visited = new WeakSet()) {
if (typeof value === "function") {
throw ErrorException("CURSOR GET_LIST candidate WHERE cannot contain function-backed values");
}
if (!value || typeof value !== "object" || visited.has(value) || value instanceof ArrayBuffer || value instanceof Date || value instanceof RegExp) {
return;
}
if (ArrayBuffer.isView(value)) {
if (typeof SharedArrayBuffer !== "undefined" && value.buffer instanceof SharedArrayBuffer) {
throw ErrorException("CURSOR GET_LIST candidate WHERE cannot contain shared-memory values");
}
return;
}
visited.add(value);
const prototype = Object.getPrototypeOf(value);
const isFindOperator = InstanceChecker.isFindOperator(value);
if (isFindOperator) {
if (prototype !== EqualOperator.prototype && prototype !== FindOperator.prototype) {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains an unsupported executable value");
}
this.assertCanonicalFindOperator(value, prototype === EqualOperator.prototype);
}
else if (!Array.isArray(value) && prototype !== null && prototype !== Object.prototype) {
throw ErrorException("CURSOR GET_LIST candidate WHERE contains an unsupported executable value");
}
for (const key of Reflect.ownKeys(value)) {
if (Array.isArray(value) && key === "length") {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor || !("value" in descriptor)) {
throw ErrorException("CURSOR GET_LIST candidate WHERE must contain data properties only");
}
this.assertStableCandidateWhere(descriptor.value, visited);
}
}
static assertSupportedCandidateProperties(properties) {
for (const key of ["join", "lock"]) {
const descriptor = ObjectFindPropertyDescriptor(properties, key);
if (descriptor && (!this.isDataProperty(descriptor) || descriptor.value !== undefined)) {
throw ErrorException(`CURSOR GET_LIST function subscriber cannot set ${key}`);
}
}
const withDeletedDescriptor = ObjectFindPropertyDescriptor(properties, "withDeleted");
if (withDeletedDescriptor && (!this.isDataProperty(withDeletedDescriptor) || (withDeletedDescriptor.value !== undefined && typeof withDeletedDescriptor.value !== "boolean"))) {
throw ErrorException("CURSOR GET_LIST function subscriber withDeleted must be a boolean data property");
}
}
static copyDataProperty(source, target, key) {
const descriptor = ObjectFindPropertyDescriptor(source, key);
if (!descriptor) {
this.defineValue(target, key);
return;
}
if (!this.isDataProperty(descriptor)) {
throw ErrorException(`Generated GET_MANY option ${String(key)} must be a data property`);
}
Object.defineProperty(target, key, ApiControllerGeneratedSecuritySnapshot.detach(descriptor));
}
static createCandidateProperties(properties) {
const candidateProperties = {};
for (const key of ["where", "withDeleted"]) {
this.copyDataProperty(properties, candidateProperties, key);
}
return ApiControllerGeneratedSecuritySnapshot.detach(candidateProperties);
}
static defineValue(target, key, value) {
Object.defineProperty(target, key, {
// eslint-disable-next-line @elsikora/typescript/naming-convention
configurable: true,
// eslint-disable-next-line @elsikora/typescript/naming-convention
enumerable: true,
value,
// eslint-disable-next-line @elsikora/typescript/naming-convention
writable: true,
});
}
static isDataProperty(descriptor) {
return "value" in descriptor;
}
static readWhere(properties) {
const descriptor = ObjectFindPropertyDescriptor(properties, "where");
if (!descriptor) {
return undefined;
}
if (!this.isDataProperty(descriptor)) {
throw ErrorException("Generated GET_MANY option where must be a data property");
}
const value = descriptor.value;
return value;
}
static resolveScopedWindowWhere(session) {
const baseWhere = this.readWhere(session.baseProperties);
const candidateWhere = session.state.candidateProperties ? this.readWhere(session.state.candidateProperties) : undefined;
const candidateScope = session.state.isCandidateWhereScoped ? candidateWhere : AuthorizationScopeMergeWhere(candidateWhere, baseWhere);
return AuthorizationScopeMergeWhere(ApiControllerGeneratedSecuritySnapshot.detach(candidateScope), ApiControllerGeneratedSecuritySnapshot.detach(session.windowWhere));
}
}
export { ApiControllerGeneratedGetManyContract };
//# sourceMappingURL=get-many-contract.class.js.map