@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
593 lines (524 loc) • 16.8 kB
text/typescript
// 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;
};
}