Skip to content

Commit f85357b

Browse files
committed
add custom mapping exemple
1 parent 59b3cba commit f85357b

3 files changed

Lines changed: 158 additions & 13 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const { createActiveRecordEntity, DataSource } = require('nosqlax');
2+
3+
const schema = {
4+
"$id": "my-schema",
5+
"$schema": "http://json-schema.org/draft-07/schema#",
6+
"type": "object",
7+
"definitions": {
8+
"address": {
9+
"type": "object",
10+
"properties": {
11+
"city": { "type": "string" }
12+
}
13+
}
14+
},
15+
"properties": {
16+
"name": { "type": "string" },
17+
"address": {
18+
"$ref": "#/definitions/address"
19+
}
20+
}
21+
}
22+
23+
// Datasource (connexion)
24+
const ds = new DataSource({
25+
url: 'http://localhost:5984',
26+
database: 'nosqlax-test'
27+
})
28+
29+
// Create the User class using the helper
30+
const User = createActiveRecordEntity(
31+
"User",
32+
"user",
33+
schema,
34+
ds,
35+
{
36+
37+
methods: {
38+
async findByCity(city) {
39+
return this.findOne({ city: { $eq: city } });
40+
},
41+
async getViewA(options) {
42+
return this.dbConnection.view('design', 'view', options)
43+
// you can also process view results here to return User entities
44+
}
45+
},
46+
fieldMap: {
47+
type: "doctype",
48+
city: "address.city"
49+
}
50+
51+
})
52+
53+
// Instanciate a user
54+
const user = new User({ name: "John Custom Field" });
55+
user.city = "Lyon"
56+
57+
// { name: 'John Custom Field', city: 'Lyon' }
58+
console.log(user.toJSON())
59+
60+
61+
// Define a service class using your entity
62+
class UserService {
63+
64+
constructor(UserClass) {
65+
this.UserClass = UserClass;
66+
}
67+
68+
69+
async findUserByCity(name) {
70+
return await this.UserClass.findByCity(name);
71+
}
72+
73+
async findUserByEmailAndName(email, name) {
74+
return await this.UserClass.findOne({
75+
"$and": [
76+
{ "name": name },
77+
{ "email": email }
78+
]
79+
}
80+
)
81+
82+
}
83+
84+
async getUserById(id) {
85+
// Fetch a user by ID
86+
return await this.UserClass.find(id);
87+
}
88+
89+
async deleteUser(id) {
90+
// Delete a user by ID
91+
return await this.UserClass.delete(id);
92+
}
93+
}
94+
95+
// instantiate service
96+
97+
const myService = new UserService(User);
98+
99+
100+
async function main() {
101+
await user.save();
102+
// Document like this will be created in DB:
103+
/* {
104+
"_id": "f9d62b31017e03b71fb0a84a5e000a08",
105+
"_rev": "1-8d43f9216da5ae302f393886daa52765",
106+
"name": "John Active",
107+
"address": {
108+
"city": "Lyon"
109+
},
110+
"type": "user"
111+
} */
112+
113+
const found = await myService.findUserByCity("Lyon");
114+
console.log(found.toJSON());
115+
/* {
116+
city: 'Lyon',
117+
name: 'John Custom Field',
118+
_id: 'f9d62b31017e03b71fb0a84a5e00f512',
119+
_rev: '1-2b06619c969d53f4afb28765595b9a5f'
120+
}
121+
*/
122+
}
123+
124+
main();

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"build": "tsc",
1616
"watch": "tsc --watch",
1717
"data-mapper-basic-example": "node example/basic-example/data-mapper-basic-example.js",
18-
"active-record-basic-example": "node example/basic-example/active-record-basic-example.js"
18+
"active-record-basic-example": "node example/basic-example/active-record-basic-example.js",
19+
"custom-field-mapping-example": "node example/custom-field-mapping/custom-field-mapping-example.js"
1920
},
2021
"keywords": [
2122
"couchdb",

src/core/BaseEntity.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,35 @@ interface IBaseEntity {
1414
}
1515
import Ajv, { ValidateFunction, ErrorObject, AnySchema, AnySchemaObject } from 'ajv';
1616

17+
function resolveRef(ref: string, definitions: Record<string, any>): any {
18+
const match = ref.match(/^#\/definitions\/(.+)$/);
19+
if (!match) throw new Error(`Unsupported $ref format: ${ref}`);
20+
const key = match[1];
21+
const resolved = definitions[key];
22+
if (!resolved) throw new Error(`Could not resolve $ref: ${ref}`);
23+
return resolved;
24+
}
25+
1726
function collectAndRemoveAllSchemas(
1827
schema: any,
1928
path: string[],
2029
key: string,
21-
results: any[]
30+
results: any[],
31+
definitions: Record<string, any>
2232
): void {
2333
if (!schema || typeof schema !== 'object') return;
2434

25-
// Base case: found the key
35+
// 🔁 If it's a $ref, resolve it
36+
if (typeof schema === 'object' && schema.$ref) {
37+
schema = resolveRef(schema.$ref, definitions);
38+
}
39+
40+
// Base case: found the key in properties
2641
if (path.length === 0 && schema.properties?.[key]) {
27-
results.push(schema.properties[key]);
42+
let fieldSchema = schema.properties[key];
43+
44+
45+
results.push(fieldSchema);
2846
delete schema.properties[key];
2947

3048
if (Array.isArray(schema.required)) {
@@ -35,38 +53,38 @@ function collectAndRemoveAllSchemas(
3553

3654
const [next, ...rest] = path;
3755

38-
// Dive into nested property
56+
// Dive into nested propertiess
3957
if (schema.properties?.[next]) {
40-
collectAndRemoveAllSchemas(schema.properties[next], rest, key, results);
58+
collectAndRemoveAllSchemas(schema.properties[next], rest, key, results, definitions);
4159
}
4260

43-
// Dive into oneOf / anyOf / allOf branches
61+
// Dive into oneOf / anyOf / allOf
4462
for (const comb of ['oneOf', 'anyOf', 'allOf']) {
4563
if (Array.isArray(schema[comb])) {
4664
for (const subSchema of schema[comb]) {
47-
collectAndRemoveAllSchemas(subSchema, path, key, results);
65+
collectAndRemoveAllSchemas(subSchema, path, key, results, definitions);
4866
}
4967
}
5068
}
5169
}
5270

53-
5471
function restructureSchemaFromFieldMap(
5572
schema: AnySchemaObject,
5673
fieldMap: Record<string, string>
5774
): AnySchemaObject {
5875
const cloned = JSON.parse(JSON.stringify(schema));
76+
const definitions = cloned.definitions || {};
5977
const toAdd: Record<string, any> = {};
6078

6179
for (const [aliasName, fieldPath] of Object.entries(fieldMap)) {
6280
const parts = fieldPath.split('.');
6381
if (parts.length <= 1) continue;
6482

65-
const propKey = parts.pop()!; // use non-null assertion
83+
const propKey = parts.pop()!;
6684
const parentPath = parts;
6785

6886
const collectedSchemas: any[] = [];
69-
collectAndRemoveAllSchemas(cloned, parentPath, propKey, collectedSchemas);
87+
collectAndRemoveAllSchemas(cloned, parentPath, propKey, collectedSchemas, definitions);
7088

7189
if (collectedSchemas.length === 1) {
7290
toAdd[aliasName] = collectedSchemas[0];
@@ -139,14 +157,16 @@ abstract class BaseEntity implements IBaseEntity {
139157
reverseMap[path] = alias;
140158
}
141159

142-
schema = restructureSchemaFromFieldMap(rawSchema, fieldMap);
160+
// compile to dereference the schema
161+
const validator =ajv.compile(rawSchema);
162+
const resolvedSchema = validator.schema as AnySchemaObject;
163+
restructureSchemaFromFieldMap(rawSchema, fieldMap);
143164

144165
const schemaProperties = schema.properties || {};
145166
const schemaDefs: any = schema.definitions;
146167

147168
for (const [schemaProp, schemaDef] of Object.entries(schemaProperties)) {
148169
const propName = reverseMap[schemaProp] || schemaProp;
149-
150170
// Inject definitions into subschema if needed
151171
if (typeof schemaDef === 'object' && schemaDefs) {
152172
(schemaDef as any).definitions = schemaDefs;

0 commit comments

Comments
 (0)