Skip to content

Commit 12860b4

Browse files
committed
fix schema parser
1 parent 16f219f commit 12860b4

3 files changed

Lines changed: 125 additions & 17 deletions

File tree

package-lock.json

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nosqlax",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "NoSQLax is a modern, lightweight JavaScript Object Document Mapper(ODM) library that makes working with CouchDB a breeze. Inspired by CouchDB’s “Relax” philosophy and the chill vibes of Snorlax, NoSQLax takes the hassle out of managing your data, offering a streamlined and intuitive repository pattern to handle CRUD operations effortlessly.",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",
@@ -40,6 +40,7 @@
4040
"dependencies": {
4141
"@apidevtools/json-schema-ref-parser": "^11.9.3",
4242
"ajv": "^8.17.1",
43+
"flatted": "^3.3.3",
4344
"nano": "^10.1.4"
4445
},
4546
"devDependencies": {

src/core/BaseEntity.ts

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11

22
type 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

510
interface IBaseEntity {
611
id?: string;
@@ -13,6 +18,44 @@ interface IBaseEntity {
1318
toJSON(): Record<string, any>;
1419
}
1520
import 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

1861
function 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
132178
const 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

Comments
 (0)