Skip to content

Commit 6d151c9

Browse files
committed
tests
1 parent 30d5687 commit 6d151c9

File tree

8 files changed

+363
-686
lines changed

8 files changed

+363
-686
lines changed

src/core/BaseEntity.ts

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
type FieldMap = Record<string, string>;
3+
34
interface 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
1674
abstract 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 {

src/core/CreateEntity.ts

Lines changed: 0 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,7 @@ type EntityConfig = {
1616
fieldMap?: Record<string, string>;
1717
};
1818

19-
type Constructor<T = {}> = new (...args: any[]) => T;
2019

21-
function capitalize(str: string): string {
22-
return str.charAt(0).toUpperCase() + str.slice(1);
23-
}
24-
25-
function restructureSchemaFromFieldMap(
26-
schema: AnySchemaObject,
27-
fieldMap: Record<string, string>
28-
): AnySchemaObject {
29-
const cloned = JSON.parse(JSON.stringify(schema)); // Deep clone to avoid mutating the original
30-
const topLevelProps = cloned.properties || {};
31-
const toAdd: Record<string, any> = {};
32-
33-
for (const [aliasName, fieldPath] of Object.entries(fieldMap)) {
34-
const parts = fieldPath.split('.');
35-
if (parts.length <= 1) continue; // Only process nested fields
36-
37-
const propKey = parts.pop()!;
38-
let current = cloned;
39-
let found = true;
40-
41-
for (const part of parts) {
42-
if (
43-
current &&
44-
typeof current === 'object' &&
45-
current.properties &&
46-
current.properties[part] &&
47-
current.properties[part].type === 'object'
48-
) {
49-
current = current.properties[part];
50-
} else {
51-
found = false;
52-
break;
53-
}
54-
}
55-
56-
if (found && current?.properties?.[propKey]) {
57-
const fieldSchema = current.properties[propKey];
58-
59-
// Add it to top-level properties with the alias name
60-
toAdd[aliasName] = fieldSchema;
61-
62-
// Remove the property from its original nested location
63-
delete current.properties[propKey];
64-
65-
// Also remove from required, if applicable
66-
if (Array.isArray(current.required)) {
67-
current.required = current.required.filter((r:String) => r !== propKey);
68-
}
69-
}
70-
}
71-
72-
// Add extracted properties to top-level schema
73-
cloned.properties = { ...topLevelProps, ...toAdd };
74-
75-
return cloned;
76-
}
7720

7821
function createEntityBase(
7922
name: string,
@@ -84,94 +27,19 @@ function createEntityBase(
8427
) {
8528
const { methods, ajvOptions = {}, fieldMap = {} } = config;
8629

87-
const ajv = new Ajv(ajvOptions);
88-
89-
// Resolve the schema
90-
let schema: AnySchemaObject | undefined;
91-
92-
if (typeof schemaOrSchemaId === 'string') {
93-
const validateFn = ajv.getSchema(schemaOrSchemaId);
94-
schema =
95-
validateFn?.schema && typeof validateFn.schema === 'object'
96-
? (validateFn.schema as AnySchemaObject)
97-
: undefined;
98-
if (!schema) {
99-
throw new Error(`Schema with ID "${schemaOrSchemaId}" not found or invalid`);
100-
}
101-
} else if (typeof schemaOrSchemaId === 'object') {
102-
schema = schemaOrSchemaId as AnySchemaObject;
103-
} else {
104-
throw new Error('Invalid schema or schema ID provided');
105-
}
106-
107-
const rawSchema = schema; //
108-
schema = restructureSchemaFromFieldMap(rawSchema, fieldMap);
109-
110-
const reverseMap: Record<string, string> = {};
111-
for (const [alias, path] of Object.entries(fieldMap)) {
112-
reverseMap[path] = alias;
113-
}
114-
115-
const validators: Record<string, ValidateFunction> = {};
11630

11731
// Dynamically define class
11832
const DynamicEntityClass = class extends EntityClass {
11933
static type = type;
12034
static schemaOrSchemaId = schemaOrSchemaId;
12135
static fieldMap = fieldMap;
122-
static schema = schema;
123-
124-
private __data: Record<string, any> = {};
12536

12637
constructor(data: Record<string, any>) {
12738
super(data);
128-
129-
// Initialize values using defined setters
130-
for (const [key, value] of Object.entries(data)) {
131-
const setterName = `set${key.charAt(0).toUpperCase()}${key.slice(1)}`;
132-
const setter = (this as any)[setterName];
133-
134-
if (typeof setter === 'function') {
135-
setter.call(this, value);
136-
}
137-
// else ignore silently
138-
}
13939
}
14040

141-
toJSON() {
142-
const base = super.toJSON ? super.toJSON() : {};
143-
const json: Record<string, any> = { ...base };
144-
145-
for (const [schemaProp, schemaDef] of Object.entries(schema.properties || {})) {
146-
const propName = reverseMap[schemaProp] || schemaProp;
147-
json[schemaProp] = this.__data[propName];
148-
}
149-
150-
return json;
151-
}
15241
};
15342

154-
// Define get/set methods based on schema
155-
for (const [schemaProp, schemaDef] of Object.entries(schema.properties || {})) {
156-
const propName = reverseMap[schemaProp] || schemaProp;
157-
const getterName = `get${capitalize(propName)}`;
158-
const setterName = `set${capitalize(propName)}`;
159-
160-
const validator = typeof schemaDef === 'object' ? ajv.compile(schemaDef as object) : undefined;
161-
if (validator) validators[propName] = validator;
162-
163-
(DynamicEntityClass.prototype as any)[getterName] = function () {
164-
return this.__data[propName];
165-
};
166-
167-
(DynamicEntityClass.prototype as any)[setterName] = function (value: any) {
168-
if (validator && !validator(value)) {
169-
const errors = validator.errors?.map(err => `${err.instancePath} ${err.message}`).join(', ');
170-
throw new Error(`Validation failed for "${propName}": ${errors}`);
171-
}
172-
this.__data[propName] = value;
173-
};
174-
}
17543

17644
// Attach custom static methods
17745
if (methods) {
@@ -183,8 +51,6 @@ function createEntityBase(
18351
return DynamicEntityClass as unknown;
18452
}
18553

186-
187-
18854
export function createActiveRecordEntity(
18955
name: string,
19056
type: string,

src/core/CreateRepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export default function createRepository(
2828
}
2929
};
3030

31-
// Attach static methods if provided
31+
// Attach instance methods if provided
3232
if (config.methods) {
33-
Object.assign(DynamicRepoClass, config.methods);
33+
Object.assign(DynamicRepoClass.prototype, config.methods);
3434
}
3535

3636
return new DynamicRepoClass();

0 commit comments

Comments
 (0)