11
22type FieldMap = Record < string , string > ;
3- import $RefParser from '@apidevtools/json-schema-ref-parser' ;
3+ import $RefParser , { dereference } from '@apidevtools/json-schema-ref-parser' ;
4+ import path from 'path' ;
5+
6+ import { parse , stringify } from 'flatted' ;
7+
8+
49
510interface IBaseEntity {
611 id ?: string ;
@@ -13,6 +18,44 @@ interface IBaseEntity {
1318 toJSON ( ) : Record < string , any > ;
1419}
1520import Ajv , { ValidateFunction , ErrorObject , AnySchema , AnySchemaObject } from 'ajv' ;
21+ import { ResolverOptions , FileInfo } from '@apidevtools/json-schema-ref-parser' ;
22+
23+ function createAjvResolver ( ajv : Ajv ) : ResolverOptions {
24+ return {
25+ order : 2 ,
26+ canRead : true , // Accept all refs (you can narrow with a RegExp or custom logic)
27+
28+ async read ( file : FileInfo ) : Promise < string > {
29+ const refPath = path . basename ( file . url ) ; // remove fragment part
30+ let schema : AnySchema | undefined ;
31+
32+ // Try full ID match
33+ const fullSchemaFn = ajv . getSchema ( refPath ) ;
34+ schema = fullSchemaFn ?. schema as AnySchema ;
35+
36+ // If not found, try suffix match
37+ if ( ! schema ) {
38+ const allSchemas = Object . values ( ajv . schemas || { } ) ;
39+ for ( const schemaObj of allSchemas ) {
40+ // `schemaObj` can be just a schema or {schema, meta, ...} depending on AJV version
41+ const candidateSchema = ( schemaObj as any ) ?. schema || ( schemaObj as AnySchema ) ;
42+ const id = ( candidateSchema as any ) ?. $id || ( candidateSchema as any ) ?. id ;
43+ if ( id && id . endsWith ( refPath ) ) {
44+ schema = candidateSchema ;
45+ break ;
46+ }
47+ }
48+ }
49+
50+ if ( ! schema ) {
51+ throw new Error ( `Schema not found for ref: ${ file . url } ` ) ;
52+ }
53+
54+ return JSON . stringify ( schema ) ;
55+ } ,
56+ } ;
57+ }
58+
1659
1760
1861function collectAndRemoveAllSchemas (
@@ -23,6 +66,9 @@ function collectAndRemoveAllSchemas(
2366) : void {
2467 if ( ! schema || typeof schema !== 'object' ) return ;
2568
69+ // remaining $ref only are circular refs after dereferencings
70+ if ( schema . $ref && typeof schema . $ref === 'string' ) return ;
71+
2672 // Base case: match at current level
2773 if ( path . length === 0 && schema . properties ?. [ key ] ) {
2874 results . push ( schema . properties [ key ] ) ;
@@ -92,7 +138,8 @@ function restructureSchemaFromFieldMap(
92138 schema : AnySchemaObject ,
93139 fieldMap : Record < string , string >
94140) : AnySchemaObject {
95- const cloned = JSON . parse ( JSON . stringify ( schema ) ) ;
141+
142+ const cloned = parse ( stringify ( schema ) ) ; // safe circular clone
96143 const toAdd : Record < string , any > = { } ;
97144
98145 // 1. Flatten all top-level combinator properties (even if not mapped)
@@ -127,7 +174,6 @@ function restructureSchemaFromFieldMap(
127174 return cloned ;
128175}
129176
130-
131177// Used to avoid redefining getters/setters for the same subclass
132178const initializedClasses = new WeakSet < Function > ( ) ;
133179
@@ -169,7 +215,7 @@ abstract class BaseEntity implements IBaseEntity {
169215 let schema : any ;
170216
171217 if ( typeof ctor . schemaOrSchemaId === 'string' ) {
172- const validateFn = ajv . getSchema ( ctor . schemaOrSchemaId ) ;
218+ const validateFn = ajv . getSchema ( ctor . schemaOrSchemaId . split ( "#/definitions/" ) [ 0 ] ) ; // we get base schema with its deifnitions for the dereferencing
173219 schema =
174220 validateFn ?. schema && typeof validateFn . schema === 'object'
175221 ? validateFn . schema as AnySchema
@@ -194,24 +240,79 @@ abstract class BaseEntity implements IBaseEntity {
194240 }
195241
196242 // compile to dereference the schema
197- const validator = ajv . compile ( rawSchema ) ;
198- const resolvedSchema = validator . schema as AnySchemaObject ;
199- const dereferencedSchema = await $RefParser . dereference ( rawSchema ) ;
200- const restructured = restructureSchemaFromFieldMap ( dereferencedSchema , fieldMap ) ;
243+
244+
245+ // 3. Dereference local $ref (like "#/definitions/...")
246+ // dereference local ref from ajv schemas
247+ const options = {
248+ resolve : {
249+ ajv : createAjvResolver ( ajv ) ,
250+ } ,
251+ dereference : {
252+ circular : 'ignore'
253+ }
254+ } as const ;
255+ const dereferencedSchema2 = await $RefParser . bundle ( rawSchema , options ) ;
256+
257+ let restructured ;
258+
259+ if ( typeof ctor . schemaOrSchemaId === 'object' ) {
260+ // schema passedas object so it's necessarely the whole object
261+ restructured = restructureSchemaFromFieldMap ( dereferencedSchema2 , fieldMap ) ;
262+ }
263+ else {
264+ const ajv = new Ajv ( { strict : false , schemas : [ dereferencedSchema2 ] } ) ;
265+
266+ const validateFn = ajv . getSchema ( ctor . schemaOrSchemaId ) ;
267+
268+ if ( ! validateFn ?. schema ) {
269+ throw new Error ( `Schema not found in AJV for ID: ${ ctor . schemaOrSchemaId } ` ) ;
270+ }
271+
272+ const finalSchema = validateFn . schema ;
273+
274+ /* if (ctor.schemaOrSchemaId.split("#/definitions/").length > 1) {
275+ (finalSchema as any).definitions = {
276+ ...(finalSchema as any).definitions,
277+ ...dereferencedSchema2.definitions
278+ }
279+ } */
280+
281+ // todo : get schema using $id with new ajv instance or validator or else
282+ restructured = restructureSchemaFromFieldMap ( finalSchema as AnySchemaObject , fieldMap ) ;
283+
284+ if ( ctor . schemaOrSchemaId . split ( "#/definitions/" ) . length > 1 ) {
285+ ( restructured as any ) . definitions = {
286+ ...( restructured as any ) . definitions ,
287+ ...dereferencedSchema2 . definitions
288+ }
289+ }
290+ }
201291
202292 const schemaProperties = restructured . properties || { } ;
203293 const schemaDefs : any = restructured . definitions ;
204-
294+
205295 for ( const [ schemaProp , schemaDef ] of Object . entries ( schemaProperties ) ) {
206296 const propName = reverseMap [ schemaProp ] || schemaProp ;
207297
208298 // Inject definitions into subschema if needed
209299 if ( typeof schemaDef === 'object' && schemaDefs ) {
210- ( schemaDef as any ) . definitions = schemaDefs ;
300+ ( schemaDef as any ) . definitions = {
301+ ...( schemaDef as any ) . definitions ,
302+ ...schemaDefs
303+ }
211304 }
212305
213- const propValidator = ajv . compile ( schemaDef as object ) ;
214-
306+ // Protect _id and _rev from being overridden
307+ if ( propName === '_id' || propName === '_rev' ) {
308+ continue ;
309+ }
310+
311+ const ajvProp = new Ajv ( {
312+ strict : false
313+ } ) ;
314+ const propValidator = ajvProp . compile ( schemaDef as object ) ;
315+
215316 Object . defineProperty ( ctor . prototype , propName , {
216317 get ( ) {
217318 return this . __data . get ( this ) ?. [ propName ] ;
@@ -227,7 +328,7 @@ abstract class BaseEntity implements IBaseEntity {
227328 . join ( ', ' ) ;
228329 throw new Error ( `Validation failed for "${ propName } ": ${ errors } ` ) ;
229330 }
230-
331+
231332 const dataStore = this . __data . get ( this ) || { } ;
232333 dataStore [ propName ] = value ;
233334 this . __data . set ( this , dataStore ) ;
@@ -236,7 +337,7 @@ abstract class BaseEntity implements IBaseEntity {
236337 configurable : false
237338 } ) ;
238339 }
239-
340+
240341 initializedClasses . add ( ctor ) ;
241342
242343 }
0 commit comments