BREAKING CHANGE(decorators): Migrate to TC39 Stage 3 decorators and refactor decorator metadata handling; update class initialization, lucene adapter fixes and docs

This commit is contained in:
2025-11-17 12:51:45 +00:00
parent d254f58a05
commit 1cd0f09598
19 changed files with 451 additions and 364 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.16.7',
version: '6.0.0',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
}

View File

@@ -26,9 +26,14 @@ const collectionFactory = new CollectionFactory();
* @param dbArg
*/
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return function classDecorator(value: Function, context: ClassDecoratorContext) {
if (context.kind !== 'class') {
throw new Error('Collection can only decorate classes');
}
// Capture original constructor for _svDbOptions forwarding
const originalConstructor = constructor as any;
const originalConstructor = value as any;
const constructor = value as { new (...args: any[]): any };
const getCollection = () => {
if (!(dbArg instanceof SmartdataDb)) {
@@ -71,7 +76,44 @@ export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
configurable: true
});
return decoratedClass;
// Initialize prototype properties from context.metadata (TC39 decorator metadata)
// This ensures prototype properties are available before any instance is created
const metadata = context.metadata as any;
if (metadata) {
const proto = decoratedClass.prototype;
// Initialize globalSaveableProperties
if (metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
}
// Initialize saveableProperties
if (metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
// Initialize uniqueIndexes
if (metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
}
// Initialize regularIndexes
if (metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
}
// Initialize searchableFields on constructor (not prototype)
if (metadata.searchableFields && !Array.isArray((decoratedClass as any).searchableFields)) {
(decoratedClass as any).searchableFields = [...metadata.searchableFields];
}
// Initialize _svDbOptions from metadata
if (metadata._svDbOptions && !originalConstructor._svDbOptions) {
originalConstructor._svDbOptions = { ...metadata._svDbOptions };
}
}
return decoratedClass as any;
};
}
@@ -89,7 +131,13 @@ export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T):
* @param dbArg
*/
export function managed<TManager extends IManager>(managerArg?: TManager | TDelayed<TManager>) {
return function classDecorator<T extends { new (...args: any[]): any }>(constructor: T) {
return function classDecorator(value: Function, context: ClassDecoratorContext) {
if (context.kind !== 'class') {
throw new Error('managed can only decorate classes');
}
const constructor = value as { new (...args: any[]): any };
const decoratedClass = class extends constructor {
public static className = constructor.name;
public static get collection() {
@@ -139,7 +187,46 @@ export function managed<TManager extends IManager>(managerArg?: TManager | TDela
return manager;
}
};
return decoratedClass;
// Initialize prototype properties from context.metadata (TC39 decorator metadata)
// This ensures prototype properties are available before any instance is created
const originalConstructor = value as any;
const metadata = context.metadata as any;
if (metadata) {
const proto = decoratedClass.prototype;
// Initialize globalSaveableProperties
if (metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
}
// Initialize saveableProperties
if (metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
// Initialize uniqueIndexes
if (metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
}
// Initialize regularIndexes
if (metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
}
// Initialize searchableFields on constructor (not prototype)
if (metadata.searchableFields && !Array.isArray((decoratedClass as any).searchableFields)) {
(decoratedClass as any).searchableFields = [...metadata.searchableFields];
}
// Initialize _svDbOptions from metadata
if (metadata._svDbOptions && !originalConstructor._svDbOptions) {
originalConstructor._svDbOptions = { ...metadata._svDbOptions };
}
}
return decoratedClass as any;
};
}

View File

@@ -28,15 +28,42 @@ export interface SearchOptions<T> {
export type TDocCreation = 'db' | 'new' | 'mixed';
// Type for decorator metadata - extends TypeScript's built-in DecoratorMetadataObject
interface ISmartdataDecoratorMetadata extends DecoratorMetadataObject {
globalSaveableProperties?: string[];
saveableProperties?: string[];
uniqueIndexes?: string[];
regularIndexes?: Array<{field: string, options: IIndexOptions}>;
searchableFields?: string[];
_svDbOptions?: Record<string, SvDbOptions>;
}
export function globalSvDb() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called svDb() on >${target.constructor.name}.${key}<`);
if (!target.globalSaveableProperties) {
target.globalSaveableProperties = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('globalSvDb can only decorate fields');
}
target.globalSaveableProperties.push(key);
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.globalSaveableProperties) {
metadata.globalSaveableProperties = [];
}
metadata.globalSaveableProperties.push(String(context.name));
logger.log('debug', `called globalSvDb() on metadata for property ${String(context.name)}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.globalSaveableProperties && !proto.globalSaveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.globalSaveableProperties = [...metadata.globalSaveableProperties];
logger.log('debug', `initialized globalSaveableProperties with ${proto.globalSaveableProperties.length} properties`);
}
});
};
}
@@ -54,20 +81,47 @@ export interface SvDbOptions {
* saveable - saveable decorator to be used on class properties
*/
export function svDb(options?: SvDbOptions) {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called svDb() on >${target.constructor.name}.${key}<`);
if (!target.saveableProperties) {
target.saveableProperties = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('svDb can only decorate fields');
}
target.saveableProperties.push(key);
// attach custom serializer/deserializer options to the class constructor
const ctor = target.constructor as any;
if (!ctor._svDbOptions) {
ctor._svDbOptions = {};
const propName = String(context.name);
// Store metadata at class level using Symbol.metadata
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
metadata.saveableProperties.push(propName);
// Store options in metadata
if (options) {
ctor._svDbOptions[key] = options;
if (!metadata._svDbOptions) {
metadata._svDbOptions = {};
}
metadata._svDbOptions[propName] = options;
}
logger.log('debug', `called svDb() on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const ctor = this.constructor;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
// Initialize prototype array from metadata (runs once per class)
proto.saveableProperties = [...metadata.saveableProperties];
logger.log('debug', `initialized saveableProperties with ${proto.saveableProperties.length} properties`);
}
// Initialize svDbOptions from metadata
if (metadata && metadata._svDbOptions && !ctor._svDbOptions) {
ctor._svDbOptions = { ...metadata._svDbOptions };
}
});
};
}
@@ -75,13 +129,30 @@ export function svDb(options?: SvDbOptions) {
* searchable - marks a property as searchable with Lucene query syntax
*/
export function searchable() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
// Attach to class constructor for direct access
const ctor = target.constructor as any;
if (!Array.isArray(ctor.searchableFields)) {
ctor.searchableFields = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('searchable can only decorate fields');
}
ctor.searchableFields.push(key);
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.searchableFields) {
metadata.searchableFields = [];
}
metadata.searchableFields.push(propName);
// Use addInitializer to set up constructor property once
context.addInitializer(function(this: any) {
const ctor = this.constructor as any;
const metadata = ctor[Symbol.metadata];
if (metadata && metadata.searchableFields && !Array.isArray(ctor.searchableFields)) {
// Initialize from metadata (runs once per class)
ctor.searchableFields = [...metadata.searchableFields];
}
});
};
}
@@ -94,20 +165,44 @@ function escapeForRegex(input: string): string {
* unique index - decorator to mark a unique index
*/
export function unI() {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called unI on >>${target.constructor.name}.${key}<<`);
// mark the index as unique
if (!target.uniqueIndexes) {
target.uniqueIndexes = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('unI can only decorate fields');
}
target.uniqueIndexes.push(key);
// and also save it
if (!target.saveableProperties) {
target.saveableProperties = [];
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.uniqueIndexes) {
metadata.uniqueIndexes = [];
}
target.saveableProperties.push(key);
metadata.uniqueIndexes.push(propName);
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
logger.log('debug', `called unI on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.uniqueIndexes && !proto.uniqueIndexes) {
proto.uniqueIndexes = [...metadata.uniqueIndexes];
logger.log('debug', `initialized uniqueIndexes with ${proto.uniqueIndexes.length} properties`);
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}
@@ -126,28 +221,47 @@ export interface IIndexOptions {
* index - decorator to mark a field for regular indexing
*/
export function index(options?: IIndexOptions) {
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
logger.log('debug', `called index() on >${target.constructor.name}.${key}<`);
// Initialize regular indexes array if it doesn't exist
if (!target.regularIndexes) {
target.regularIndexes = [];
return (value: undefined, context: ClassFieldDecoratorContext) => {
if (context.kind !== 'field') {
throw new Error('index can only decorate fields');
}
// Add this field to regularIndexes with its options
target.regularIndexes.push({
field: key,
const propName = String(context.name);
// Store metadata at class level
const metadata = context.metadata as ISmartdataDecoratorMetadata;
if (!metadata.regularIndexes) {
metadata.regularIndexes = [];
}
metadata.regularIndexes.push({
field: propName,
options: options || {}
});
// Also ensure it's marked as saveable
if (!target.saveableProperties) {
target.saveableProperties = [];
// Also mark as saveable
if (!metadata.saveableProperties) {
metadata.saveableProperties = [];
}
if (!target.saveableProperties.includes(key)) {
target.saveableProperties.push(key);
if (!metadata.saveableProperties.includes(propName)) {
metadata.saveableProperties.push(propName);
}
logger.log('debug', `called index() on metadata for property ${propName}`);
// Use addInitializer to ensure prototype arrays are set up once
context.addInitializer(function(this: any) {
const proto = this.constructor.prototype;
const metadata = this.constructor[Symbol.metadata];
if (metadata && metadata.regularIndexes && !proto.regularIndexes) {
proto.regularIndexes = [...metadata.regularIndexes];
logger.log('debug', `initialized regularIndexes with ${proto.regularIndexes.length} indexes`);
}
if (metadata && metadata.saveableProperties && !proto.saveableProperties) {
proto.saveableProperties = [...metadata.saveableProperties];
}
});
};
}