Skip to content

Commit d661c58

Browse files
committed
feat: toDTO method, improved logging, log error when accessing a partial model
1 parent a2cddce commit d661c58

File tree

24 files changed

+442
-203
lines changed

24 files changed

+442
-203
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
node: ["18", "20"]
12+
node: ["18", "20", "22"]
1313
name: Node ${{ matrix.node }} sample
1414
steps:
1515
- uses: actions/checkout@v2

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
All notable changes to this project made by Monade Team are documented in this file. For info refer to team@monade.io
44

5+
## [2.0.0] - 2025-01-24
6+
### Added
7+
- When resolving a model not present in the included array, it returns an error when trying to access it's properties.
8+
- Added a new method `toDTO` to the `Model` class, that returns a DTO object with the model's properties, replacing relationships with ids.
9+
- Added a max depth to the `toJSON` method, to avoid infinite loops when serializing relationships.
10+
11+
### Changed
12+
- BREAKING: the debug now returns a tag (error, warn, info) before the message, allowing to distinguish between error types.
13+
514
## [1.1.0] - 2023-09-03
615

716
### Added

dist/index.cjs.js

Lines changed: 109 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,50 @@
22

33
Object.defineProperty(exports, '__esModule', { value: true });
44

5+
const $registeredModels = [];
6+
const $registeredAttributes = [];
7+
const $registeredRelationships = [];
8+
59
class Model {
6-
toJSON() {
7-
return { ...this
10+
toDTO() {
11+
const response = {
12+
...this
13+
};
14+
delete response._type;
15+
const rels = $registeredRelationships.find(e => e.klass === this.constructor) ?? {
16+
attributes: {}
817
};
18+
for (const key in response) {
19+
if (rels.attributes[key] || response[key] instanceof Model) {
20+
response[`${key}Id`] = response[key]?.id ?? null;
21+
delete response[key];
22+
}
23+
}
24+
return response;
25+
}
26+
toJSON(maxDepth = 100) {
27+
const response = {
28+
...this
29+
};
30+
delete response._type;
31+
for (const key in response) {
32+
if (response[key] instanceof Model) {
33+
if (maxDepth <= 0) {
34+
delete response[key];
35+
} else {
36+
response[key] = response[key].toJSON(maxDepth - 1);
37+
}
38+
}
39+
}
40+
return response;
941
}
10-
1142
toFormData() {
1243
const data = this.toJSON();
1344
const formData = new FormData();
14-
1545
for (const key in data) {
1646
if (data[key] === null || data[key] === undefined) {
1747
continue;
1848
}
19-
2049
if (Array.isArray(data[key])) {
2150
for (const value of data[key]) {
2251
formData.append(key + "[]", value);
@@ -27,47 +56,49 @@ class Model {
2756
formData.append(key, data[key]);
2857
}
2958
}
30-
3159
return formData;
3260
}
33-
3461
}
3562

36-
function debug(...args) {
37-
debug.adapter(...args);
63+
function debug(level, ...args) {
64+
debug.adapter(level, ...args);
3865
}
39-
40-
debug.adapter = (...args) => console.warn(...args);
66+
debug.adapter = (level, ...args) => {
67+
switch (level) {
68+
case 'warn':
69+
console.warn(...args);
70+
break;
71+
case 'error':
72+
console.error(...args);
73+
break;
74+
case 'info':
75+
default:
76+
console.log(...args);
77+
break;
78+
}
79+
};
4180

4281
class Parser {
43-
static $registeredModels = [];
44-
static $registeredAttributes = [];
45-
static $registeredRelationships = [];
4682
resolved = {};
47-
4883
constructor(data, included = []) {
4984
this.data = data;
5085
this.included = included;
5186
}
52-
5387
run() {
5488
if (!this.data) {
5589
return null;
5690
}
57-
5891
const {
5992
data,
6093
included
6194
} = this;
6295
const fullIncluded = Array.isArray(data) ? [...data, ...included] : [data, ...included];
6396
return this.parse(data, fullIncluded);
6497
}
65-
6698
parse(data, included = []) {
6799
if (!data) {
68100
return null;
69101
}
70-
71102
if (Array.isArray(data)) {
72103
return this.parseList(data, included);
73104
} else if ("data" in data && !("id" in data)) {
@@ -76,119 +107,126 @@ class Parser {
76107
return this.parseElement(data, included);
77108
}
78109
}
79-
80110
parseList(list, included) {
81111
return list.map(e => {
82112
return this.parseElement(e, included);
83113
});
84114
}
85-
86115
parseElement(element, included) {
87116
const uniqueKey = `${element.id}$${element.type}`;
88-
89117
if (this.resolved[uniqueKey]) {
90118
return this.resolved[uniqueKey];
91119
}
92-
93120
const loadedElement = Parser.load(element, included);
94-
const model = Parser.$registeredModels.find(e => e.type === loadedElement.type);
95-
const attrData = Parser.$registeredAttributes.find(e => e.klass === model?.klass);
96-
const relsData = Parser.$registeredRelationships.find(e => e.klass === model?.klass);
97-
const instance = new (model?.klass || Model)();
121+
const model = $registeredModels.find(e => e.type === loadedElement.type);
122+
const instance = this.wrapWhenPartial(new (model?.klass || Model)(), loadedElement);
98123
this.resolved[uniqueKey] = instance;
124+
if (model && model.createFn) {
125+
return model.createFn(instance, loadedElement, relation => this.parse(relation, included));
126+
}
127+
const attrData = $registeredAttributes.find(e => e.klass === model?.klass);
128+
const relsData = $registeredRelationships.find(e => e.klass === model?.klass);
99129
instance.id = loadedElement.id;
100-
101-
for (const key in loadedElement.attributes) {
102-
const parser = attrData?.attributes?.[key];
103-
130+
instance._type = loadedElement.type;
131+
this.parseAttributes(instance, loadedElement, attrData);
132+
this.parseRelationships(instance, loadedElement, relsData, included);
133+
return instance;
134+
}
135+
wrapWhenPartial(instance, loadedElement) {
136+
if (loadedElement.$_partial) {
137+
return new Proxy(instance, {
138+
get: function (target, prop) {
139+
if (prop in target) {
140+
return target[prop];
141+
}
142+
if (prop === "$_partial") {
143+
return true;
144+
}
145+
debug('error', `Trying to call property "${prop.toString()}" to a model that is not included. Add "${loadedElement.type}" to included models.`);
146+
return undefined;
147+
}
148+
});
149+
}
150+
return instance;
151+
}
152+
parseRelationships(instance, loadedElement, relsData, included) {
153+
for (const key in loadedElement.relationships) {
154+
const relation = loadedElement.relationships[key];
155+
const parser = relsData?.attributes?.[key];
104156
if (parser) {
105-
instance[parser.key] = parser.parser(loadedElement.attributes[key]);
157+
instance[parser.key] = parser.parser(this.parse(relation, included));
106158
} else {
107-
instance[key] = loadedElement.attributes[key];
108-
debug(`Undeclared key "${key}" in "${loadedElement.type}"`);
159+
instance[key] = this.parse(relation, included);
160+
debug('warn', `Undeclared relationship "${key}" in "${loadedElement.type}"`);
109161
}
110162
}
111-
112-
if (attrData) {
113-
for (const key in attrData.attributes) {
114-
const parser = attrData.attributes[key];
115-
163+
if (relsData) {
164+
for (const key in relsData.attributes) {
165+
const parser = relsData.attributes[key];
116166
if (!(parser.key in instance)) {
117167
if ("default" in parser) {
118168
instance[parser.key] = parser.default;
119169
} else {
120-
debug(`Missing attribute "${key}" in "${loadedElement.type}"`);
170+
debug('warn', `Missing relationships "${key}" in "${loadedElement.type}"`);
121171
}
122172
}
123173
}
124174
}
125-
126-
for (const key in loadedElement.relationships) {
127-
const relation = loadedElement.relationships[key];
128-
const parser = relsData?.attributes?.[key];
129-
175+
}
176+
parseAttributes(instance, loadedElement, attrData) {
177+
for (const key in loadedElement.attributes) {
178+
const parser = attrData?.attributes?.[key];
130179
if (parser) {
131-
instance[parser.key] = parser.parser(this.parse(relation, included));
180+
instance[parser.key] = parser.parser(loadedElement.attributes[key]);
132181
} else {
133-
instance[key] = this.parse(relation, included);
134-
debug(`Undeclared relationship "${key}" in "${loadedElement.type}"`);
182+
instance[key] = loadedElement.attributes[key];
183+
debug('warn', `Undeclared key "${key}" in "${loadedElement.type}"`);
135184
}
136185
}
137-
138-
if (relsData) {
139-
for (const key in relsData.attributes) {
140-
const parser = relsData.attributes[key];
141-
186+
if (attrData) {
187+
for (const key in attrData.attributes) {
188+
const parser = attrData.attributes[key];
142189
if (!(parser.key in instance)) {
143190
if ("default" in parser) {
144191
instance[parser.key] = parser.default;
145192
} else {
146-
debug(`Missing relationships "${key}" in "${loadedElement.type}"`);
193+
debug('warn', `Missing attribute "${key}" in "${loadedElement.type}"`);
147194
}
148195
}
149196
}
150197
}
151-
152-
return instance;
153198
}
154-
155199
static load(element, included) {
156200
const found = included.find(e => e.id == element.id && e.type === element.type);
157-
158201
if (!found) {
159-
debug(`Relationship with type ${element.type} with id ${element.id} not present in included`);
202+
debug('info', `Relationship with type ${element.type} with id ${element.id} not present in included`);
160203
}
161-
162-
return found || { ...element,
204+
return found || {
205+
...element,
163206
$_partial: true
164207
};
165208
}
166-
167209
}
168210

169211
function Attr(sourceKey, options = {
170212
parser: v => v
171213
}) {
172214
return function _Attr(klass, key) {
173-
let model = Parser.$registeredAttributes.find(e => e.klass === klass.constructor);
174-
215+
let model = $registeredAttributes.find(e => e.klass === klass.constructor);
175216
if (!model) {
176217
model = {
177218
attributes: {},
178219
klass: klass.constructor
179220
};
180-
Parser.$registeredAttributes.push(model);
221+
$registeredAttributes.push(model);
181222
}
182-
183223
const data = {
184224
parser: options.parser ?? (v => v),
185225
key
186226
};
187-
188227
if ("default" in options) {
189228
data.default = options.default;
190229
}
191-
192230
model.attributes[sourceKey ?? key] = data;
193231
};
194232
}
@@ -197,16 +235,14 @@ function Rel(sourceKey, options = {
197235
parser: v => v
198236
}) {
199237
return function _Rel(klass, key) {
200-
let model = Parser.$registeredRelationships.find(e => e.klass === klass.constructor);
201-
238+
let model = $registeredRelationships.find(e => e.klass === klass.constructor);
202239
if (!model) {
203240
model = {
204241
attributes: {},
205242
klass: klass.constructor
206243
};
207-
Parser.$registeredRelationships.push(model);
244+
$registeredRelationships.push(model);
208245
}
209-
210246
model.attributes[sourceKey ?? key] = {
211247
parser: options.parser ?? (v => v),
212248
key,
@@ -217,7 +253,7 @@ function Rel(sourceKey, options = {
217253

218254
function JSONAPI(type) {
219255
return function _JSONAPI(constructor) {
220-
Parser.$registeredModels.push({
256+
$registeredModels.push({
221257
klass: constructor,
222258
type
223259
});

0 commit comments

Comments
 (0)