UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

593 lines (524 loc) 16.8 kB
// core/decorators.ts // Base decorator types for database-agnostic schema definition import "reflect-metadata"; import type { EntityOptions, PropertyOptions, RelationshipOptions, OneToOneOptions, OneToManyOptions, ManyToOneOptions, ManyToManyOptions, RelationshipTypeFunc, ValidationRule, MinMaxOptions, PatternOptions, EmailOptions, URLOptions, CustomValidationOptions, LengthOptions, IndexOptions, TimestampOptions, } from "./types"; // Helper function to get constructor function getConstructor(target: any): Function { return target.constructor || target; } /** * Entity decorator - marks a class as an entity to be persisted * * This decorator no longer directly registers with a registry. * Instead, it stores metadata on the class that can be later * processed by SchemaBuilder. */ export function Entity(options: EntityOptions = {}) { return function <T extends { new (...args: any[]): any }>(target: T): T { // Store entity metadata on the class Reflect.defineMetadata("entity:options", options, target); // Initialize empty arrays for property and relationship tracking if they don't exist if (!Reflect.hasMetadata("entity:properties", target)) { Reflect.defineMetadata("entity:properties", [], target); } if (!Reflect.hasMetadata("entity:relationships", target)) { Reflect.defineMetadata("entity:relationships", [], target); } return target; }; } /** * Labels decorator - assigns semantic labels to an entity * * Labels are database-agnostic and describe what an entity represents. * Different adapters can use labels differently: * - Neo4j: Node labels * - PostgreSQL: Table names, tags * - MongoDB: Collection names, document types */ export function Labels(labels: string[]) { return function<T extends { new (...args: any[]): {} }>(constructor: T) { Reflect.defineMetadata('entity:labels', labels, constructor); return constructor; }; } /** * Id decorator - marks a property as a primary identifier * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function Id(options: PropertyOptions = {}) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Mark as ID property Reflect.defineMetadata("property:id", true, target, propertyKey); // Also register as a property with enhanced metadata const enhancedOptions = { required: true, unique: true, ...options, }; Reflect.defineMetadata("property:options", enhancedOptions, target, propertyKey); // Add to the list of properties on the class const properties: string[] = Reflect.getMetadata("entity:properties", constructor) || []; if (!properties.includes(propertyKey)) { properties.push(propertyKey); Reflect.defineMetadata("entity:properties", properties, constructor); } }; } /** * Property decorator - marks a property to be persisted * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function Property(options: PropertyOptions = {}) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Store property metadata Reflect.defineMetadata("property:options", options, target, propertyKey); // Add to the list of properties on the class const properties: string[] = Reflect.getMetadata("entity:properties", constructor) || []; if (!properties.includes(propertyKey)) { properties.push(propertyKey); Reflect.defineMetadata("entity:properties", properties, constructor); } }; } /** * OneToOne decorator - defines a one-to-one relationship * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function OneToOne<T>(options: OneToOneOptions<T>) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Get name from options or use property key const name = options.name || propertyKey.toUpperCase(); // Store relationship options metadata const relationshipOptions = { name, target: options.target, // Store the function itself, don't call it yet inverse: options.inverse, required: options.required, }; Reflect.defineMetadata( "relationship:options", relationshipOptions, target, propertyKey ); // Store the metadata about this being a OneToOne relationship Reflect.defineMetadata( "relationship:type", "one-to-one", target, propertyKey ); // Store database-specific options if (options.neo4j) { Reflect.defineMetadata( "neo4j:relationship", options.neo4j, target, propertyKey ); } if (options.mongodb) { Reflect.defineMetadata( "mongodb:relationship", options.mongodb, target, propertyKey ); } if (options.postgresql) { Reflect.defineMetadata( "postgresql:relationship", options.postgresql, target, propertyKey ); } // Add to the list of relationships on the class const relationships: string[] = Reflect.getMetadata("entity:relationships", constructor) || []; if (!relationships.includes(propertyKey)) { relationships.push(propertyKey); Reflect.defineMetadata("entity:relationships", relationships, constructor); } }; } /** * OneToMany decorator - defines a one-to-many relationship * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function OneToMany<T>(options: OneToManyOptions<T>) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Get name from options or use property key const name = options.name || propertyKey.toUpperCase(); // Store relationship options metadata const relationshipOptions = { name, target: options.target, // Store the function itself, don't call it yet inverse: options.inverse, required: options.required, }; Reflect.defineMetadata( "relationship:options", relationshipOptions, target, propertyKey ); // Store the metadata about this being a OneToMany relationship Reflect.defineMetadata( "relationship:type", "one-to-many", target, propertyKey ); // Store database-specific options if (options.neo4j) { Reflect.defineMetadata( "neo4j:relationship", options.neo4j, target, propertyKey ); } if (options.mongodb) { Reflect.defineMetadata( "mongodb:relationship", options.mongodb, target, propertyKey ); } if (options.postgresql) { Reflect.defineMetadata( "postgresql:relationship", options.postgresql, target, propertyKey ); } // Add to the list of relationships on the class const relationships: string[] = Reflect.getMetadata("entity:relationships", constructor) || []; if (!relationships.includes(propertyKey)) { relationships.push(propertyKey); Reflect.defineMetadata("entity:relationships", relationships, constructor); } }; } /** * ManyToOne decorator - defines a many-to-one relationship * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function ManyToOne<T>(options: ManyToOneOptions<T>) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Get name from options or use property key const name = options.name || propertyKey.toUpperCase(); // Store relationship options metadata const relationshipOptions = { name, target: options.target, // Store the function itself, don't call it yet inverse: options.inverse, required: options.required, }; Reflect.defineMetadata( "relationship:options", relationshipOptions, target, propertyKey ); // Store the metadata about this being a ManyToOne relationship Reflect.defineMetadata( "relationship:type", "many-to-one", target, propertyKey ); // Store database-specific options if (options.neo4j) { Reflect.defineMetadata( "neo4j:relationship", options.neo4j, target, propertyKey ); } if (options.mongodb) { Reflect.defineMetadata( "mongodb:relationship", options.mongodb, target, propertyKey ); } if (options.postgresql) { Reflect.defineMetadata( "postgresql:relationship", options.postgresql, target, propertyKey ); } // Add to the list of relationships on the class const relationships: string[] = Reflect.getMetadata("entity:relationships", constructor) || []; if (!relationships.includes(propertyKey)) { relationships.push(propertyKey); Reflect.defineMetadata("entity:relationships", relationships, constructor); } }; } /** * ManyToMany decorator - defines a many-to-many relationship * * This decorator stores metadata on the property that can be later * processed by SchemaBuilder. */ export function ManyToMany<T>(options: ManyToManyOptions<T>) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Get name from options or use property key const name = options.name || propertyKey.toUpperCase(); // Store relationship options metadata const relationshipOptions = { name, target: options.target, // Store the function itself, don't call it yet inverse: options.inverse, required: options.required, }; Reflect.defineMetadata( "relationship:options", relationshipOptions, target, propertyKey ); // Store the metadata about this being a ManyToMany relationship Reflect.defineMetadata( "relationship:type", "many-to-many", target, propertyKey ); // Store database-specific options if (options.neo4j) { Reflect.defineMetadata( "neo4j:relationship", options.neo4j, target, propertyKey ); } if (options.mongodb) { Reflect.defineMetadata( "mongodb:relationship", options.mongodb, target, propertyKey ); } if (options.postgresql) { Reflect.defineMetadata( "postgresql:relationship", options.postgresql, target, propertyKey ); } // Add to the list of relationships on the class const relationships: string[] = Reflect.getMetadata("entity:relationships", constructor) || []; if (!relationships.includes(propertyKey)) { relationships.push(propertyKey); Reflect.defineMetadata("entity:relationships", relationships, constructor); } }; } // Helper function to add validation rules function addValidationRule(target: any, propertyKey: string, rule: ValidationRule) { const constructor = getConstructor(target); const existingRules: ValidationRule[] = Reflect.getMetadata("validation:rules", target, propertyKey) || []; existingRules.push(rule); Reflect.defineMetadata("validation:rules", existingRules, target, propertyKey); } /** * Min decorator - validates that a numeric value is greater than or equal to specified minimum */ export function Min(options: MinMaxOptions) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "min", options, message: options.message || `Value must be greater than or equal to ${options.value}`, }); }; } /** * Max decorator - validates that a numeric value is less than or equal to specified maximum */ export function Max(options: MinMaxOptions) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "max", options, message: options.message || `Value must be less than or equal to ${options.value}`, }); }; } /** * Pattern decorator - validates that a string matches a regular expression pattern */ export function Pattern(options: PatternOptions) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "pattern", options, message: options.message || `Value must match pattern ${options.pattern.toString()}`, }); }; } /** * Email decorator - validates that a string is a valid email address */ export function Email(options: EmailOptions = {}) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "email", options, message: options.message || "Value must be a valid email address", }); }; } /** * URL decorator - validates that a string is a valid URL */ export function URL(options: URLOptions = {}) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "url", options, message: options.message || "Value must be a valid URL", }); }; } /** * Custom decorator - validates using a custom validation function */ export function Custom(options: CustomValidationOptions) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "custom", options, message: options.message || "Value failed custom validation", }); }; } /** * MinLength decorator - validates that a string or array has minimum length */ export function MinLength(length: number, message?: string) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "minLength", options: { value: length }, message: message || `Value must have at least ${length} characters`, }); }; } /** * MaxLength decorator - validates that a string or array has maximum length */ export function MaxLength(length: number, message?: string) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "maxLength", options: { value: length }, message: message || `Value must have at most ${length} characters`, }); }; } /** * Length decorator - validates that a string or array has length within specified range */ export function Length(options: LengthOptions) { return function (target: any, propertyKey: string) { addValidationRule(target, propertyKey, { type: "length", options, message: options.message || `Value length must be between ${options.min || 0} and ${options.max || 'unlimited'}`, }); }; } /** * Index decorator - marks a property to be indexed in the database */ export function Index(options: IndexOptions = {}) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Store index metadata Reflect.defineMetadata("index:options", options, target, propertyKey); // Add to the list of indexed properties on the class const indexes: string[] = Reflect.getMetadata("entity:indexes", constructor) || []; if (!indexes.includes(propertyKey)) { indexes.push(propertyKey); Reflect.defineMetadata("entity:indexes", indexes, constructor); } }; } /** * Timestamp decorator - automatically manages timestamp fields */ export function Timestamp(options: TimestampOptions = {}) { return function (target: any, propertyKey: string) { const constructor = getConstructor(target); // Store timestamp metadata Reflect.defineMetadata("timestamp:options", options, target, propertyKey); // Add to the list of timestamp properties on the class const timestamps: string[] = Reflect.getMetadata("entity:timestamps", constructor) || []; if (!timestamps.includes(propertyKey)) { timestamps.push(propertyKey); Reflect.defineMetadata("entity:timestamps", timestamps, constructor); } }; } /** * Auth decorator - defines permission requirements for an entity */ export interface AuthOptions { roles?: string[]; permissions?: string[]; } export function Auth(options: AuthOptions = {}) { return function <T extends { new (...args: any[]): any }>(target: T): T { // Store auth metadata on the class Reflect.defineMetadata("auth:options", options, target); return target; }; }