11
22type FieldMap = Record < string , string > ;
3+
34interface IBaseEntity {
45 id ?: string ;
56 rev ?: string ;
@@ -10,7 +11,64 @@ interface IBaseEntity {
1011 // Convert entity to JSON object
1112 toJSON ( ) : Record < string , any > ;
1213}
13- import Ajv , { } from 'ajv' ; // Import Ajv types
14+ import Ajv , { ValidateFunction , ErrorObject , AnySchema , AnySchemaObject } from 'ajv' ;
15+
16+
17+ function restructureSchemaFromFieldMap (
18+ schema : AnySchemaObject ,
19+ fieldMap : Record < string , string >
20+ ) : AnySchemaObject {
21+ const cloned = JSON . parse ( JSON . stringify ( schema ) ) ; // Deep clone to avoid mutating the original
22+ const topLevelProps = cloned . properties || { } ;
23+ const toAdd : Record < string , any > = { } ;
24+
25+ for ( const [ aliasName , fieldPath ] of Object . entries ( fieldMap ) ) {
26+ const parts = fieldPath . split ( '.' ) ;
27+ if ( parts . length <= 1 ) continue ; // Only process nested fields
28+
29+ const propKey = parts . pop ( ) ! ;
30+ let current = cloned ;
31+ let found = true ;
32+
33+ for ( const part of parts ) {
34+ if (
35+ current &&
36+ typeof current === 'object' &&
37+ current . properties &&
38+ current . properties [ part ] &&
39+ current . properties [ part ] . type === 'object'
40+ ) {
41+ current = current . properties [ part ] ;
42+ } else {
43+ found = false ;
44+ break ;
45+ }
46+ }
47+
48+ if ( found && current ?. properties ?. [ propKey ] ) {
49+ const fieldSchema = current . properties [ propKey ] ;
50+
51+ // Add it to top-level properties with the alias name
52+ toAdd [ aliasName ] = fieldSchema ;
53+
54+ // Remove the property from its original nested location
55+ delete current . properties [ propKey ] ;
56+
57+ // Also remove from required, if applicable
58+ if ( Array . isArray ( current . required ) ) {
59+ current . required = current . required . filter ( ( r :String ) => r !== propKey ) ;
60+ }
61+ }
62+ }
63+
64+ // Add extracted properties to top-level schema
65+ cloned . properties = { ...topLevelProps , ...toAdd } ;
66+
67+ return cloned ;
68+ }
69+
70+ // Used to avoid redefining getters/setters for the same subclass
71+ const initializedClasses = new WeakSet < Function > ( ) ;
1472
1573// src/BaseEntity.ts
1674abstract class BaseEntity implements IBaseEntity {
@@ -19,54 +77,118 @@ abstract class BaseEntity implements IBaseEntity {
1977
2078 static type : string ;
2179 static schemaOrSchemaId : string | object ;
80+ private __data : WeakMap < any , Record < string , any > > = new WeakMap ( ) ;
81+ private validators : Record < string , ValidateFunction > = { } ;
2282
23- // Store private values in a WeakMap
24- private privateData : WeakMap < any , any > ;
2583
2684 // Index signature to allow dynamic properties
2785 [ key : string ] : any ; // This allows dynamic fields to be assigned to the instance
2886
2987 // Map from entity attributes to document fields, type is implicitly handled
3088 static fieldMap : Record < string , string > = { type : "type" } ; // Default fieldMap, type is implicitly required
3189
32- constructor ( data : { _id ?: string ; _rev ?: string ; [ key : string ] : any } ) {
90+
91+
92+ constructor ( data : { _id ?: string ; _rev ?: string ; [ key : string ] : any } = { } ) {
3393 this . _id = data . _id ;
3494 this . _rev = data . _rev ;
95+ this . __data . set ( this , { } ) ;
3596
36- this . privateData = new WeakMap ( ) ;
37- this . privateData . set ( this , { } ) ;
97+ const ctor = this . constructor as typeof BaseEntity ;
3898
39- // Ensure schemaOrSchemaId is defined
40- if ( ( this . constructor as typeof BaseEntity ) . schemaOrSchemaId === undefined ) {
41- throw new Error ( `${ this . constructor . name } must define schemaOrSchemaId` ) ;
42- }
43- if ( ( this . constructor as typeof BaseEntity ) . type === undefined ) {
44- throw new Error ( `${ this . constructor . name } must define type` ) ;
99+ if ( initializedClasses . has ( ctor ) ) {
100+ this . initializeData ( data ) ;
101+ return ;
45102 }
46103
47- // Initialize the AJV instance with options
48104 const ajv = new Ajv ( { } ) ;
49- let schema : any = this . schemaOrSchemaId ;
50-
51- // If schemaOrSchemaId is a string (schema ID), fetch the schema
52- if ( typeof schema === "string" ) {
53- schema = ajv . getSchema ( schema ) ?. schema ; // Retrieve the schema
105+ let schema : any ;
106+
107+ if ( typeof ctor . schemaOrSchemaId === 'string' ) {
108+ const validateFn = ajv . getSchema ( ctor . schemaOrSchemaId ) ;
109+ schema =
110+ validateFn ?. schema && typeof validateFn . schema === 'object'
111+ ? validateFn . schema as AnySchema
112+ : undefined ;
54113 if ( ! schema ) {
55- throw new Error ( `Schema with ID ${ schema } not found. ` ) ;
114+ throw new Error ( `Schema with ID " ${ ctor . schemaOrSchemaId } " not found or invalid ` ) ;
56115 }
116+ } else if ( typeof ctor . schemaOrSchemaId === 'object' ) {
117+ schema = ctor . schemaOrSchemaId as AnySchema ;
118+ } else {
119+ throw new Error ( 'Invalid schema or schema ID provided' ) ;
120+ }
121+
122+
123+ // Replace with actual restructuring if you support it
124+ const rawSchema = schema ;
125+
126+ const fieldMap = ctor . fieldMap || { } ;
127+ const reverseMap : Record < string , string > = { } ;
128+ for ( const [ alias , path ] of Object . entries ( fieldMap ) ) {
129+ reverseMap [ path ] = alias ;
130+ }
131+
132+ schema = restructureSchemaFromFieldMap ( rawSchema , fieldMap ) ;
133+
134+ const schemaProperties = schema . properties || { } ;
135+
136+ for ( const [ schemaProp , schemaDef ] of Object . entries ( schemaProperties ) ) {
137+ const propName = reverseMap [ schemaProp ] || schemaProp ;
138+
139+ const validator : ValidateFunction | undefined =
140+ typeof schemaDef === 'object' ? ajv . compile ( schemaDef as object ) : undefined ;
141+ if ( validator ) this . validators [ propName ] = validator ;
142+
143+ Object . defineProperty ( ctor . prototype , propName , {
144+ get : function ( ) {
145+ return this . __data . get ( this ) ?. [ propName ] ;
146+ } ,
147+ set : function ( value : any ) {
148+ const validator = this . validators [ propName ] ;
149+ if ( validator && ! validator ( value ) ) {
150+ const errors = ( validator . errors as ErrorObject [ ] | null | undefined )
151+ ?. map ( ( err ) => `${ err . instancePath } ${ err . message } ` )
152+ . join ( ', ' ) ;
153+ throw new Error ( `Validation failed for "${ propName } ": ${ errors } ` ) ;
154+ }
155+ const dataStore = this . __data . get ( this ) || { } ;
156+ dataStore [ propName ] = value ;
157+ this . __data . set ( this , dataStore ) ;
158+ } ,
159+ enumerable : true ,
160+ configurable : false ,
161+ } ) ;
57162 }
58163
59- const schemaProperties = schema ?. properties || { } ;
164+ initializedClasses . add ( ctor ) ;
60165
166+ this . initializeData ( data ) ;
61167 }
62168
63169 toJSON ( ) {
64- const data = { ...this . privateData . get ( this ) } ;
170+ const data = { ...( this . __data . get ( this ) || { } ) } ;
65171 if ( this . _id ) data . _id = this . _id ;
66172 if ( this . _rev ) data . _rev = this . _rev ;
67173 return data ;
68174 }
69175
176+ private initializeData ( data : Record < string , any > ) {
177+ const fieldMap = ( this . constructor as typeof BaseEntity ) . fieldMap || { } ;
178+ const reverseMap : Record < string , string > = { } ;
179+ for ( const [ alias , path ] of Object . entries ( fieldMap ) ) {
180+ reverseMap [ path ] = alias ;
181+ }
182+
183+ for ( const [ key , value ] of Object . entries ( data ) ) {
184+ try {
185+ ( this as any ) [ key ] = value ;
186+ } catch {
187+ // Skip properties without setters (e.g., unknown fields)
188+ }
189+ }
190+ }
191+
70192
71193 // Getter for id
72194 get id ( ) : string | undefined {
0 commit comments