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

@@ -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];
}
});
};
}