Skip to content

Commit e3ac5ae

Browse files
committed
wip
1 parent dc9ad57 commit e3ac5ae

9 files changed

Lines changed: 335 additions & 189 deletions

src/core/ActiveRecordEntity.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ abstract class ActiveRecordEntity extends BaseEntity {
1818
// Auto-attach DataSource & initialize repo when a child class is defined
1919
protected static attachDataSource(dataSource: DataSource, ajvOptions: any) {
2020
if (!this.repoMap.has(this)) {
21-
this.repoMap.set(this, createRepository(dataSource, ajvOptions, this as any));
21+
this.repoMap.set(this, createRepository(this as any, dataSource, {ajvOptions} ));
2222
}
2323
}
2424

@@ -78,6 +78,12 @@ abstract class ActiveRecordEntity extends BaseEntity {
7878
return await (this.constructor as typeof ActiveRecordEntity).getRepo().delete(this.id);
7979
}
8080

81+
// Define the extend method to add new query methods
82+
static extend(newMethods: Record<string, Function>) {
83+
Object.assign(this, newMethods);
84+
}
85+
86+
8187
}
8288

8389
export default ActiveRecordEntity;

src/core/BaseEntity.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1+
2+
type FieldMap = Record<string, string>;
3+
import Ajv, { } from 'ajv'; // Import Ajv types
4+
15
// src/BaseEntity.ts
26
abstract class BaseEntity {
3-
private _id?: string;
7+
private _id?: string;
48
private _rev?: string;
59

6-
static type: string;
10+
static type: string;
711
static schemaOrSchemaId: string | object;
8-
12+
13+
// Store private values in a WeakMap
14+
private privateData: WeakMap<any, any>;
15+
16+
// Index signature to allow dynamic properties
17+
[key: string]: any; // This allows dynamic fields to be assigned to the instance
18+
919
// Map from entity attributes to document fields, type is implicitly handled
1020
static fieldMap: Record<string, string> = { type: "type" }; // Default fieldMap, type is implicitly required
1121

12-
constructor(data: { _id?: string; _rev?: string; [key: string]: any }) {
13-
this._id = data._id;
22+
constructor(data: { _id?: string; _rev?: string;[key: string]: any }) {
23+
this._id = data._id;
1424
this._rev = data._rev;
1525

26+
this.privateData = new WeakMap();
27+
this.privateData.set(this, {});
28+
1629
// Ensure schemaOrSchemaId is defined
1730
if ((this.constructor as typeof BaseEntity).schemaOrSchemaId === undefined) {
1831
throw new Error(`${this.constructor.name} must define schemaOrSchemaId`);
@@ -21,9 +34,44 @@ abstract class BaseEntity {
2134
throw new Error(`${this.constructor.name} must define type`);
2235
}
2336

24-
2537
}
2638

39+
toJSON() {
40+
const data = { ...this.privateData.get(this) } ;
41+
if (this._id) data._id = this._id;
42+
if (this._rev) data._rev = this._rev;
43+
return data;
44+
}
45+
46+
static extractFieldMapFromSchema(schemaOrSchemaId: any, ajvOptions: any): Record<string, string> {
47+
48+
const fieldMap: Record<string, string> = {};
49+
50+
// Initialize the AJV instance with options
51+
const ajv = new Ajv(ajvOptions || {});
52+
let schema: any = schemaOrSchemaId;
53+
54+
55+
// If schemaOrSchemaId is a string (schema ID), fetch the schema
56+
if (typeof schemaOrSchemaId === "string") {
57+
schema = ajv.getSchema(schemaOrSchemaId); // Retrieve the schema
58+
if (!schema) {
59+
throw new Error(`Schema with ID ${schemaOrSchemaId} not found.`);
60+
}
61+
}
62+
63+
if (schema && schema.properties) {
64+
// Only consider top-level properties from the schema
65+
for (const key in schema.properties) {
66+
fieldMap[key] = key; // Field name matches property name by default
67+
}
68+
}
69+
70+
return fieldMap;
71+
72+
}
73+
74+
2775
// Getter for id
2876
get id(): string | undefined {
2977
return this._id;

src/core/CouchRepository.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ abstract class CouchRepository {
202202

203203
}
204204

205+
// Define the extend method to add new query methods
206+
extend(newMethods: Record<string, Function>) {
207+
Object.assign(this, newMethods);
208+
}
209+
205210
get dataSource() {
206211
return this._dataSource;
207212
}
@@ -289,7 +294,7 @@ abstract class CouchRepository {
289294

290295
// Merge existing data with new data
291296
const transformedData = transformToDocumentFormat(
292-
{ ...existingDoc, ...data },
297+
{ ...existingDoc?.toJSON(), ...data?.toJSON() },
293298
this.entityClass,
294299
this.fieldMap
295300
);

src/core/CreateActiveRecordEntity.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/core/CreateDataMapperEntity.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/core/CreateEntity.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import ActiveRecordEntity from "./ActiveRecordEntity";
2+
import BaseEntity from "./BaseEntity";
3+
import DataMapperEntity from "./DataMapperEntity";
4+
import DataSource from "./DataSource";
5+
import Ajv, { } from 'ajv'; // Import Ajv types
6+
7+
8+
type DataMapperEntityConfig = {
9+
ajvOptions?: any; // Optional
10+
fieldMap?: Record<string, string>;
11+
};
12+
13+
type EntityConfig = {
14+
methods?: Record<string, Function>; // Optional
15+
ajvOptions?: any; // Optional
16+
fieldMap?: Record<string, string>;
17+
};
18+
19+
type Constructor<T = {}> = new (...args: any[]) => T;
20+
21+
function createEntityBase(
22+
name: string,
23+
type: string,
24+
schemaOrSchemaId: string | object,
25+
config: EntityConfig,
26+
EntityClass: typeof ActiveRecordEntity | typeof DataMapperEntity
27+
) {
28+
const { methods, ajvOptions, fieldMap = {} } = config;
29+
30+
// Merge user-provided fieldMap with the one extracted from the schema
31+
const mergedFieldMap = {
32+
...EntityClass.extractFieldMapFromSchema(schemaOrSchemaId, ajvOptions),
33+
...{ type: fieldMap?.type }
34+
};
35+
36+
// Initialize the AJV instance with options
37+
const ajv = new Ajv(ajvOptions || {});
38+
let schema: any = schemaOrSchemaId;
39+
40+
// If schemaOrSchemaId is a string (schema ID), fetch the schema
41+
if (typeof schema === "string") {
42+
schema = ajv.getSchema(schema)?.schema; // Retrieve the schema
43+
if (!schema) {
44+
throw new Error(`Schema with ID ${schema} not found.`);
45+
}
46+
}
47+
48+
const schemaProperties = schema?.properties || {};
49+
50+
// Create a dynamic subclass of the selected entity class
51+
const DynamicEntityClass = class extends EntityClass {
52+
static type = type;
53+
static schemaOrSchemaId = schemaOrSchemaId;
54+
static fieldMap = mergedFieldMap;
55+
56+
[key: string]: any; // This allows dynamic fields to be assigned to the instance
57+
58+
constructor(data: { _id?: string; _rev?: string;[key: string]: any }) {
59+
super(data);
60+
61+
// Initialize properties from schema
62+
Object.keys(schemaProperties).forEach(property => {
63+
this.privateData.get(this)[property] = data[property];
64+
});
65+
}
66+
67+
68+
69+
};
70+
71+
72+
// Assign the actual class name to the dynamically created class
73+
Object.defineProperty(DynamicEntityClass, 'name', { value: name });
74+
75+
// Dynamically add getters and setters based on schema
76+
Object.keys(schemaProperties).forEach(property => {
77+
Object.defineProperty(DynamicEntityClass.prototype, property, {
78+
get() {
79+
return this.privateData.get(this)[property]; // Retrieve the field value
80+
},
81+
set(value) {
82+
this.privateData.get(this)[property] = value; // Assign value to internal variable
83+
},
84+
enumerable: true,
85+
configurable: true
86+
});
87+
});
88+
89+
// Attach static methods if provided
90+
if (methods) {
91+
Object.assign(DynamicEntityClass, methods);
92+
}
93+
94+
return DynamicEntityClass as unknown;
95+
}
96+
97+
98+
export function createActiveRecordEntity(
99+
name: string,
100+
type: string,
101+
schemaOrSchemaId: string | object,
102+
dataSource: DataSource,
103+
config: EntityConfig
104+
) {
105+
// Explicitly type the EntityClass as typeof ActiveRecordEntity
106+
let EntityClass = createEntityBase(name, type, schemaOrSchemaId, config, ActiveRecordEntity) as typeof ActiveRecordEntity;
107+
108+
// Automatically attach the dataSource with optional ajvOptions
109+
EntityClass['attachDataSource'](dataSource, config.ajvOptions || {});
110+
111+
return EntityClass as typeof ActiveRecordEntity;
112+
}
113+
114+
115+
116+
export function createDataMapperEntity(
117+
name: string,
118+
type: string,
119+
schemaOrSchemaId: string | object,
120+
config: DataMapperEntityConfig
121+
) {
122+
// Use the shared base function for creating the class
123+
const EntityClass = createEntityBase(name, type, schemaOrSchemaId, config, DataMapperEntity) as typeof DataMapperEntity;
124+
125+
126+
return EntityClass as typeof DataMapperEntity;
127+
}

src/core/CreateRepository.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import BaseEntity from "./BaseEntity";
22
import DataSource from "./DataSource";
33
import CouchRepository from "./CouchRepository";
44

5+
type RepoConfig = {
6+
methods?: Record<string, Function>; // Optional
7+
ajvOptions?: any; // Optional
8+
};
9+
510
// Type for the entity class constructor
611
type EntityClass = {
712
fieldMap: Record<string, string>;
@@ -11,6 +16,22 @@ type EntityClass = {
1116
[key: string]: any;
1217
};
1318

14-
export default function createRepository(ds: DataSource, ajvOptions: any, entityClass: EntityClass): CouchRepository {
15-
return new (class extends CouchRepository { })(ds, ajvOptions, entityClass);
19+
export default function createRepository(
20+
entityClass: EntityClass,
21+
ds: DataSource,
22+
config: RepoConfig,
23+
): CouchRepository {
24+
// Create the dynamic repository class
25+
const DynamicRepoClass = class extends CouchRepository {
26+
constructor() {
27+
super(ds, config?.ajvOptions, entityClass);
28+
}
29+
};
30+
31+
// Attach static methods if provided
32+
if (config.methods) {
33+
Object.assign(DynamicRepoClass, config.methods);
34+
}
35+
36+
return new DynamicRepoClass();
1637
}

0 commit comments

Comments
 (0)