Skip to content

Commit e8917ad

Browse files
committed
Merge branch 'feature/2-implement-schema-field-mapping'
Close #2
2 parents bb16f7c + 90e960b commit e8917ad

6 files changed

Lines changed: 166 additions & 59 deletions

File tree

example/src/models/article.model.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ export const ArticleSchema = new Schema<IArticle>({
2525
},
2626

2727
relationships: {
28-
author: {},
28+
author: {
29+
type: 'people',
30+
},
2931

30-
comments: {},
32+
comments: {
33+
type: 'comments',
34+
},
3135
},
3236
});
3337

example/src/models/comment.model.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ export const CommentSchema = new Schema<IComment>({
2525
},
2626

2727
relationships: {
28-
article: {},
28+
article: {
29+
type: 'articles',
30+
},
2931

30-
author: {},
32+
author: {
33+
type: 'people',
34+
},
3135
},
3236
});
3337

example/src/models/people.model.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ export const PeopleSchema = new Schema<IPeople>({
3131
},
3232

3333
relationships: {
34-
articles: {},
34+
articles: {
35+
type: 'articles',
36+
},
3537

36-
comments: {},
38+
comments: {
39+
type: 'comments',
40+
},
3741
},
3842
});
3943

src/lib/model.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ BaseModel.fromJsonApi = function (body) {
192192
}
193193

194194
// Attributes
195-
for (let [attribute, value] of Object.entries(body.data.attributes ?? {})) {
196-
const property = schema.attributes[attribute];
195+
for (let [name, value] of Object.entries(body.data.attributes ?? {})) {
196+
const [attribute, property] = Object.entries(schema.attributes)
197+
.find(([_, property]) => property?.name === name)
198+
?? [name, schema.attributes[name]];
197199

198200
if (property?.type === Date && value) {
199201
value = new Date(value);
@@ -203,7 +205,11 @@ BaseModel.fromJsonApi = function (body) {
203205
}
204206

205207
// Relationships
206-
for (const [relationship, value] of Object.entries(body.data.relationships ?? {})) {
208+
for (const [name, value] of Object.entries(body.data.relationships ?? {})) {
209+
const [relationship, property] = Object.entries(schema.relationships)
210+
.find(([_, property]) => property?.name === name)
211+
?? [name, schema.relationships[name]];
212+
207213
if (Array.isArray(value.data)) {
208214
const related = value.data.map((identifier) => {
209215
const model = models[identifier.type];
@@ -473,24 +479,28 @@ BaseModel.prototype.toJsonApi = function () {
473479
}
474480
}
475481

476-
data.attributes![attribute] = value;
482+
const name = property?.name ?? attribute;
483+
484+
data.attributes![name] = value;
477485
}
478486

479487
for (const [relationship, property] of Object.entries(this.schema.relationships)) {
480488
if (!this.isNew && !this.isModified(relationship)) continue;
481489

482490
const value = this.get(relationship) as ModelInstance<Record<string, any>> | ModelInstance<Record<string, any>>[] | null;
483491

492+
const name = property?.name ?? relationship;
493+
484494
if (Array.isArray(value)) {
485-
data.relationships![relationship] = {
495+
data.relationships![name] = {
486496
data: value.map((val) => val.identifier()),
487497
};
488498
} else if (value) {
489-
data.relationships![relationship] = {
499+
data.relationships![name] = {
490500
data: value.identifier(),
491501
};
492502
} else {
493-
data.relationships![relationship] = {
503+
data.relationships![name] = {
494504
data: null,
495505
};
496506
}

src/lib/query.ts

Lines changed: 119 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { JsonApiBody, JsonApiResource } from "../types/jsonapi.type";
2-
import { fromJsonApi, Model, ModelConstructor, ModelInstance } from "./model";
1+
import { JsonApiBody, JsonApiQueryParams, JsonApiResource } from "../types/jsonapi.type";
2+
import { fromJsonApi, Model, ModelConstructor, ModelInstance, models } from "./model";
33
import Schema from "./schema";
44

55
type RawResultType<T> = {
@@ -54,6 +54,8 @@ class Query<ResultType, DocType> {
5454
this.init(model);
5555
}
5656

57+
buildParams!: () => JsonApiQueryParams;
58+
5759
catch!: Promise<ResultType>['catch'];
5860

5961
exec!: () => Promise<ResultType>;
@@ -129,62 +131,133 @@ Query.prototype.init = function (model) {
129131
this.options = {};
130132
};
131133

132-
Query.prototype.catch = function (reject) {
133-
return this.exec().then(null, reject);
134-
};
134+
Query.prototype.buildParams = function () {
135+
const options = this.getOptions();
135136

136-
Query.prototype.exec = async function exec() {
137-
const client = this.model.client;
137+
const getJsonApiName = (
138+
key: string,
139+
schema?: Schema<any>,
140+
): string => {
141+
if (schema?.attributes[key]) {
142+
return schema.attributes[key].name ?? key;
143+
} else if (schema?.relationships[key]) {
144+
return schema.relationships[key].name ?? key;
145+
}
146+
return key;
147+
};
138148

139-
const flattenIncludeQuery = (obj: IncludeQuery<any>, prefix = ''): string[] => {
140-
return Object.entries(obj).reduce((acc, [key, value]) => {
141-
const path = prefix ? `${prefix}.${key}` : key;
142-
143-
if (typeof value === 'boolean') {
144-
return value
145-
? acc.concat(path)
146-
: acc;
147-
} else {
148-
const childPaths = flattenIncludeQuery(value ?? {}, path);
149-
return childPaths.length > 0
150-
? acc.concat(childPaths)
151-
: acc.concat(path);
152-
}
153-
}, [] as string[]);
149+
const buildFilterParams = (
150+
filter: FilterQuery<any>,
151+
schema?: Schema<any>,
152+
): JsonApiQueryParams['filter'] => {
153+
return Object.fromEntries(
154+
Object.entries(filter).map(([key, value]) => [getJsonApiName(key, schema), value])
155+
);
154156
};
155157

156-
const options = this.getOptions();
157-
const params = {
158-
filter: options.filter,
159-
include: options.include
160-
? flattenIncludeQuery(options.include).join(',')
161-
: undefined,
162-
fields: options.fields
163-
? Object.fromEntries(
164-
Object.entries(options.fields).map(([type, fields]) => [type, fields.join(',')])
165-
)
166-
: undefined,
167-
sort: options.sort
168-
? Object.entries(options.sort)
169-
.map(([field, order]) => {
170-
if (order === -1 || order === 'desc' || order === 'descending') {
171-
return `-${field}`;
172-
}
173-
return field;
174-
})
175-
.join(',')
176-
: undefined,
158+
const buildIncludeParams = (
159+
include: IncludeQuery<any>,
160+
schema?: Schema<any>,
161+
): JsonApiQueryParams['include'] => {
162+
const flattenInclude = (
163+
obj: IncludeQuery<any>,
164+
schema?: Schema<any>,
165+
prefix = '',
166+
): string[] => {
167+
return Object.entries(obj).flatMap(([key, value]) => {
168+
let name = schema?.relationships[key]?.name ?? key;
169+
const path = prefix ? `${prefix}.${name}` : name;
170+
171+
if (typeof value === 'boolean') {
172+
return value ? [path] : [];
173+
}
174+
175+
const subType = schema?.relationships[key]?.type;
176+
const subSchema = Array.isArray(subType)
177+
? models[subType[0]].schema
178+
: subType
179+
? models[subType].schema
180+
: undefined;
181+
182+
const subPaths = flattenInclude(value ?? {}, subSchema, path);
183+
return subPaths.length ? subPaths : [path];
184+
});
185+
};
186+
187+
return flattenInclude(include, schema).join(',');
188+
};
189+
190+
const buildFieldsParams = (
191+
fields: FieldsQuery,
192+
): JsonApiQueryParams['fields'] => {
193+
return Object.fromEntries(
194+
Object.entries(fields).map(([type, list]) => {
195+
const schema = models[type]?.schema;
196+
197+
const names = list
198+
.map((field) => getJsonApiName(field, schema))
199+
.join(',');
200+
201+
return [type, names];
202+
})
203+
);
204+
};
205+
206+
const buildSortParams = (
207+
sort: SortQuery<any>,
208+
schema?: Schema<any>,
209+
): JsonApiQueryParams['sort'] => {
210+
return Object.entries(sort)
211+
.map(([key, order]) => {
212+
let name = getJsonApiName(key, schema)
213+
214+
if (order === -1 || order === 'desc' || order === 'descending') {
215+
return `-${name}`;
216+
}
217+
return name;
218+
})
219+
.join(',');
220+
};
221+
222+
const getSchema = () => {
223+
if (options.op === 'findRelationship' && options.related) {
224+
const property = this.model.schema.relationships[options.related];
225+
return Array.isArray(property?.type)
226+
? models[property.type[0]].schema
227+
: property?.type
228+
? models[property.type].schema
229+
: undefined;
230+
}
231+
return this.model.schema;
232+
};
233+
const schema = getSchema();
234+
235+
return {
236+
filter: options.filter ? buildFilterParams(options.filter, schema) : undefined,
237+
include: options.include ? buildIncludeParams(options.include, schema) : undefined,
238+
fields: options.fields ? buildFieldsParams(options.fields) : undefined,
239+
sort: options.sort ? buildSortParams(options.sort, schema) : undefined,
177240
page: {
178241
limit: options.limit,
179242
offset: options.offset,
180243
},
181244
};
245+
};
246+
247+
Query.prototype.catch = function (reject) {
248+
return this.exec().then(null, reject);
249+
};
250+
251+
Query.prototype.exec = async function exec() {
252+
const client = this.model.client;
253+
254+
const options = this.getOptions();
182255

183256
if (options.op === 'find') {
184257
const response = await client.client.get<JsonApiBody<JsonApiResource[]>>(
185258
`/${this.model.type}`,
186259
{
187-
params: params,
260+
params: this.buildParams(),
188261
}
189262
);
190263

@@ -200,7 +273,7 @@ Query.prototype.exec = async function exec() {
200273
const response = await client.client.get<JsonApiBody<JsonApiResource | null>>(
201274
`/${this.model.type}/${options.id}`,
202275
{
203-
params: params,
276+
params: this.buildParams(),
204277
}
205278
);
206279

@@ -216,7 +289,7 @@ Query.prototype.exec = async function exec() {
216289
const response = await client.client.get<JsonApiBody<JsonApiResource | null>>(
217290
`/${this.model.type}/${options.id}/${options.related}`,
218291
{
219-
params: params,
292+
params: this.buildParams(),
220293
}
221294
);
222295

src/lib/schema.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ type AttributesDefinition<DocType> = {
88
}
99

1010
type AttributeDefinition<T> = {
11+
/** The JSON:API attribute name. */
12+
name?: string;
13+
1114
type?: typeof Date;
1215

1316
/** The default value for this property. */
@@ -31,6 +34,15 @@ type RelationshipsDefinition<DocType> = {
3134
}
3235

3336
type RelationshipDefinition<T> = {
37+
/**
38+
* The JSON:API relationship type.
39+
* Only used to find the JSON:API relationship `name` when calling `.include()`.
40+
*/
41+
type?: string | string[];
42+
43+
/** The JSON:API relationship name. */
44+
name?: string;
45+
3446
/** The default value for this property. */
3547
default?: T | (() => T);
3648

0 commit comments

Comments
 (0)