diff --git a/packages/metadata/index.ts b/packages/metadata/index.ts new file mode 100644 index 0000000..31c9fd4 --- /dev/null +++ b/packages/metadata/index.ts @@ -0,0 +1,146 @@ +import { Decorator, MetadataKey, MetadataParameterIndex, MetadataTarget, MetadataValue } from './types/metadata.types'; +import ReflectStorage from './storage/storage-metadata'; +import { constructKey, MetadataScope, MetadataScopeType, resolveTargetClass } from './utils/metadta.utils'; +export * from './interface/metadata.interface'; +export * from './types/metadata.types'; +class Reflector { + private storage: ReflectStorage; + constructor() { + this.storage = new ReflectStorage(); + } + + /** + * Define metadata for a target and optional property. + */ + defineMetadata(metadataKey: MetadataKey, value: T, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): void { + const key = constructKey(metadataKey, propertyKey, parameterIndex); + const actualTarget = resolveTargetClass(target); + this.storage.set(actualTarget!, key, value); + } + + /** + * Retrieve metadata for a target and optional property. + */ + getMetadata(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): T | unknown { + const key = constructKey(metadataKey, propertyKey, parameterIndex); + let currentTarget = resolveTargetClass(target); + while (currentTarget) { + const metadataMap = this.storage.get(currentTarget); + + if (metadataMap && metadataMap.has(key)) { + return metadataMap.get(key); + } + currentTarget = Object.getPrototypeOf(currentTarget); + } + return undefined; + } + + /** + * Check if metadata exists for a target and optional property. + */ + hasMetadata(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): boolean { + const key = constructKey(metadataKey, propertyKey, parameterIndex); + return this.storage.has(target, key); + } + + /** + * Delete metadata for a target and optional property. + */ + deleteMetadata(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): boolean { + const key = constructKey(metadataKey, propertyKey, parameterIndex); + const actualTarget = resolveTargetClass(target); + if (!actualTarget) { + return false; + } + return this.storage.delete(actualTarget, key); + } + + /** + * Clear all metadata for a target. + */ + clearMetadata(target: MetadataTarget): void { + const actualTarget = resolveTargetClass(target); + if (!actualTarget) { + throw new Error('Unable to clear metadata: The provided target is invalid or not properly defined.'); + } + this.storage.clear(actualTarget); + } + // Decorator factory + metadata(key: MetadataKey, value: MetadataValue): Decorator { + return (target: MetadataTarget, propertyKey?: MetadataKey, descriptorOrIndex?: MetadataParameterIndex | TypedPropertyDescriptor) => { + if (typeof descriptorOrIndex === 'number') { + // Parameter decorator (target, prop, index) + this.defineMetadata(key, value, target, propertyKey, descriptorOrIndex); + } else if (typeof descriptorOrIndex === 'object') { + // Method decorator (target, prop, descriptor) + this.defineMetadata(key, value, target, propertyKey); + } else if (typeof propertyKey === 'undefined') { + // Class decorator (target) + this.defineMetadata(key, value, target); + } else { + // Property decorator (target, prop) + this.defineMetadata(key, value, target, propertyKey); + } + }; + } + /** + * List all metadata keys for a target and optional property. + */ + getMetadataKeys(target: MetadataTarget, scope?: MetadataScopeType, propertyName?: string, parameterIndex?: number): MetadataKey[] { + const resolvedTarget = resolveTargetClass(target); + const metadataMap = this.storage.get(resolvedTarget!); + + if (!metadataMap) { + return []; + } + + return Array.from(metadataMap.keys()) + .filter((key) => { + const segments = String(key).split(':'); + const currentScope = segments[0]; + + if (scope && currentScope !== MetadataScope[scope.toUpperCase()]) { + return false; + } + + if (propertyName && segments.length > 1 && segments[1] !== propertyName) { + return false; + } + + if (typeof parameterIndex === 'number') { + const hasParamSegment = segments.length > 3 && segments[2] === 'param'; + const hasCorrectIndex = hasParamSegment && Number(segments[3]) === parameterIndex; + + if (!hasCorrectIndex) { + return false; + } + } + + return true; + }) + .map((key) => { + const segments = String(key).split(':'); + return segments[segments.length - 1]; + }); + } + + /** + * List all metadata for a target. + */ + listMetadata(target: MetadataTarget): Map | null { + const metadataMap = this.storage.list(target); + if (metadataMap) { + return metadataMap as Map; + } + return null; + } + + /** + * List all metadata across all targets. + */ + listAllMetadata(): Map> { + return this.storage.allList(); + } +} + +export default new Reflector(); diff --git a/packages/metadata/interface/metadata.interface.ts b/packages/metadata/interface/metadata.interface.ts new file mode 100644 index 0000000..e1d616e --- /dev/null +++ b/packages/metadata/interface/metadata.interface.ts @@ -0,0 +1,12 @@ +import { MetadataKey, MetadataTarget, MetadataValue } from '../types/metadata.types'; + +export interface MetadataStorage { + get(target: MetadataTarget): Map; + set(target: MetadataTarget, key: MetadataKey, value: MetadataValue): void; + has(target: MetadataTarget, key: MetadataKey): boolean; + delete(target: MetadataTarget, key: MetadataKey): boolean; + clear(target: MetadataTarget): void; + keys(): string[]; + list(target: MetadataTarget): Map | null; + allList(): Map>; +} \ No newline at end of file diff --git a/packages/metadata/storage/storage-metadata.ts b/packages/metadata/storage/storage-metadata.ts new file mode 100644 index 0000000..deafc15 --- /dev/null +++ b/packages/metadata/storage/storage-metadata.ts @@ -0,0 +1,92 @@ +import { isClass } from '@gland/common/utils'; +import { MemoryCacheStore } from '@gland/cache'; +import { MetadataStorage } from '../interface/metadata.interface'; +import { MetadataKey, MetadataTarget, MetadataValue } from '../types/metadata.types'; + +class ReflectStorage implements MetadataStorage { + private storage = new MemoryCacheStore>(); + constructor() { + this.storage = new MemoryCacheStore({ + policy: 'LRU', + }); + } + private getTargetIdentifier(target: MetadataTarget): string { + if (typeof target === 'function') { + return isClass(target) ? `class:${target.name}` : `function:${target.name}@${target.length}`; + } + if (target.constructor && target.constructor.prototype === target) { + return `prototype:${target.constructor.name}`; + } + return `instance:${target.constructor.name}`; + } + + private ensureMetadataMap(target: MetadataTarget): Map { + const targetKey = this.getTargetIdentifier(target); + let metadataMap = this.storage.get(targetKey); + if (!(metadataMap instanceof Map)) { + metadataMap = new Map(); + this.storage.set(targetKey, metadataMap); + } + return metadataMap; + } + get(target: MetadataTarget): Map { + const targetKey = this.getTargetIdentifier(target); + const metadataMap = this.storage.get(targetKey); + return metadataMap instanceof Map ? metadataMap : new Map(); + } + + set(target: MetadataTarget, key: MetadataKey, value: MetadataValue): void { + const metadataMap = this.ensureMetadataMap(target); + metadataMap.set(key, value); + const targetKey = this.getTargetIdentifier(target); + this.storage.set(targetKey, metadataMap); + } + + has(target: MetadataTarget, key: MetadataKey): boolean { + const metadataMap = this.get(target); + return metadataMap.has(key); + } + + delete(target: MetadataTarget, key: MetadataKey): boolean { + const targetKey = this.getTargetIdentifier(target); + const metadataMap = this.get(target); + const result = metadataMap.delete(key); + if (metadataMap.size === 0) { + this.storage.delete(targetKey); + } else { + this.storage.set(targetKey, metadataMap); + } + + return result; + } + clear(target: MetadataTarget): void { + const targetKey = this.getTargetIdentifier(target); + this.storage.delete(targetKey); + } + + list(target: MetadataTarget): Map | null { + const targetKey = this.getTargetIdentifier(target); + const metadataMap = this.storage.get(targetKey); + if (metadataMap instanceof Map) { + return metadataMap; + } + return null; + } + keys(): string[] { + return Array.from(this.storage.keys()); + } + + allList(): Map> { + const entries = this.storage.get('*', { pattern: true, withTuples: true }) as [string, Map][]; + const allMetadata = new Map>(); + + for (const [key, value] of entries) { + if (value instanceof Map) { + allMetadata.set(key, value); + } + } + return allMetadata; + } +} + +export default ReflectStorage; diff --git a/packages/metadata/types/metadata.types.ts b/packages/metadata/types/metadata.types.ts new file mode 100644 index 0000000..a93d03e --- /dev/null +++ b/packages/metadata/types/metadata.types.ts @@ -0,0 +1,6 @@ +export type MetadataKey = string | symbol; +export type MetadataValue = unknown; +export type MetadataTarget = object | Function; +export type MetadataPropertyKey = string | symbol; +export type MetadataParameterIndex = number; +export type Decorator = ClassDecorator & PropertyDecorator & MethodDecorator & ParameterDecorator; diff --git a/packages/metadata/utils/metadta.utils.ts b/packages/metadata/utils/metadta.utils.ts new file mode 100644 index 0000000..d144fd9 --- /dev/null +++ b/packages/metadata/utils/metadta.utils.ts @@ -0,0 +1,42 @@ +import { isClass } from '@gland/common'; +import { MetadataKey, MetadataParameterIndex, MetadataTarget } from '../types/metadata.types'; +export const MetadataScope = { + CLASS: 'class', + METHOD: 'method', + PROPERTY: 'property', + PARAMETER: 'param', +} as const; +export type MetadataScopeType = keyof typeof MetadataScope; + +/** + * Construct a fully qualified metadata key. + */ +export function constructKey(metadataKey: MetadataKey, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): MetadataKey { + let key = String(metadataKey); + if (typeof parameterIndex === 'number') { + key = `${MetadataScope.PARAMETER}:${parameterIndex}:${key}`; + } + if (typeof propertyKey !== 'undefined') { + if (typeof parameterIndex === 'undefined') { + key = `${MetadataScope.METHOD}:${String(propertyKey)}:${key}`; // This is used for method metadata + } else { + key = `${MetadataScope.PROPERTY}:${String(propertyKey)}:${key}`; // Correct prefix for property metadata + } + } + + if (typeof propertyKey === 'undefined' && !parameterIndex) { + key = `${MetadataScope.CLASS}:${key}`; + } + return key; +} +/** + * Resolves the target to its class constructor. + * If the target is a class, it is returned as-is. + * If the target is an instance, its constructor is returned. + * + * @param target - The target to resolve (class or instance). + * @returns The resolved class constructor. + */ +export function resolveTargetClass(target: MetadataTarget): MetadataTarget | null { + return isClass(target) ? target : target.constructor; +}