@tryghost/jsonapi-mapper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +103 -0
- package/cjs/bookshelf/extras.js +30 -0
- package/cjs/bookshelf/index.js +74 -0
- package/cjs/bookshelf/links.js +112 -0
- package/cjs/bookshelf/utils.js +168 -0
- package/cjs/index.js +20 -0
- package/cjs/interfaces/common.js +16 -0
- package/cjs/interfaces/index.js +20 -0
- package/cjs/interfaces/links.js +16 -0
- package/cjs/interfaces/relations.js +16 -0
- package/cjs/serializer/index.js +35 -0
- package/cjs/serializer/jsonapi-serializer.skel.d.js +1 -0
- package/es/bookshelf/extras.js +10 -0
- package/es/bookshelf/index.js +55 -0
- package/es/bookshelf/links.js +92 -0
- package/es/bookshelf/utils.js +171 -0
- package/es/index.js +3 -0
- package/es/interfaces/common.js +0 -0
- package/es/interfaces/index.js +3 -0
- package/es/interfaces/links.js +0 -0
- package/es/interfaces/relations.js +0 -0
- package/es/serializer/index.js +5 -0
- package/es/serializer/jsonapi-serializer.skel.d.js +0 -0
- package/package.json +88 -0
- package/src/bookshelf/extras.ts +77 -0
- package/src/bookshelf/index.ts +64 -0
- package/src/bookshelf/links.ts +139 -0
- package/src/bookshelf/utils.ts +279 -0
- package/src/index.ts +3 -0
- package/src/interfaces/common.ts +38 -0
- package/src/interfaces/index.ts +3 -0
- package/src/interfaces/links.ts +26 -0
- package/src/interfaces/relations.ts +24 -0
- package/src/serializer/index.ts +50 -0
- package/src/serializer/jsonapi-serializer.skel.d.ts +4 -0
- package/types/bookshelf/extras.d.ts +52 -0
- package/types/bookshelf/extras.d.ts.map +1 -0
- package/types/bookshelf/index.d.ts +22 -0
- package/types/bookshelf/index.d.ts.map +1 -0
- package/types/bookshelf/links.d.ts +19 -0
- package/types/bookshelf/links.d.ts.map +1 -0
- package/types/bookshelf/utils.d.ts +26 -0
- package/types/bookshelf/utils.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/interfaces/common.d.ts +24 -0
- package/types/interfaces/common.d.ts.map +1 -0
- package/types/interfaces/index.d.ts +4 -0
- package/types/interfaces/index.d.ts.map +1 -0
- package/types/interfaces/links.d.ts +25 -0
- package/types/interfaces/links.d.ts.map +1 -0
- package/types/interfaces/relations.d.ts +22 -0
- package/types/interfaces/relations.d.ts.map +1 -0
- package/types/serializer/index.d.ts +34 -0
- package/types/serializer/index.d.ts.map +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { assign, omit, isEmpty, isNil } from 'lodash';
|
|
2
|
+
import { pluralize as plural } from 'inflection';
|
|
3
|
+
import { stringify as queryParams } from 'qs';
|
|
4
|
+
|
|
5
|
+
import { Model } from './extras';
|
|
6
|
+
import { LinkOpts, PagOpts, QueryOpts } from '../interfaces';
|
|
7
|
+
import { LinkObj } from '../serializer';
|
|
8
|
+
|
|
9
|
+
function urlConcat(...parts: string[]): string {
|
|
10
|
+
return parts.join('/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates top level links object, for primary data and pagination links.
|
|
15
|
+
*/
|
|
16
|
+
export function topLinks(linkOpts: LinkOpts): LinkObj {
|
|
17
|
+
let { baseUrl, type, pag }: LinkOpts = linkOpts;
|
|
18
|
+
|
|
19
|
+
let obj: LinkObj = {
|
|
20
|
+
self: urlConcat(baseUrl, plural(type))
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Build pagination if available
|
|
24
|
+
if (!isNil(pag)) {
|
|
25
|
+
|
|
26
|
+
// Support Bookshelf's built-in paging parameters
|
|
27
|
+
if (!isNil(pag.rowCount)) {
|
|
28
|
+
pag.total = pag.rowCount;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only add pagination links when more than 1 page
|
|
32
|
+
if (!isNil(pag.total) && pag.total > 0 && pag.total > pag.limit) {
|
|
33
|
+
assign(obj, pagLinks(linkOpts));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create links object, for pagination links.
|
|
42
|
+
* Since its used only inside other functions in this model, its not exported
|
|
43
|
+
*/
|
|
44
|
+
function pagLinks(linkOpts: LinkOpts): LinkObj | undefined {
|
|
45
|
+
let { baseUrl, type, pag, query = {} }: LinkOpts = linkOpts;
|
|
46
|
+
|
|
47
|
+
if (pag === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { offset, limit, total}: PagOpts = pag;
|
|
52
|
+
// All links are based on the resource type
|
|
53
|
+
let baseLink: string = urlConcat(baseUrl, plural(type));
|
|
54
|
+
|
|
55
|
+
// Stringify the query string without page element
|
|
56
|
+
query = omit(query, ['page', 'page[limit]', 'page[offset]']) as QueryOpts;
|
|
57
|
+
baseLink = baseLink + '?' + queryParams(query, {encode: false});
|
|
58
|
+
|
|
59
|
+
let obj: LinkObj = {} as LinkObj;
|
|
60
|
+
|
|
61
|
+
// Add leading pag links if not at the first page
|
|
62
|
+
if (offset > 0) {
|
|
63
|
+
obj.first = () => {
|
|
64
|
+
let page: any = {page: {limit, offset: 0}};
|
|
65
|
+
return baseLink + queryParams(page, {encode: false});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
obj.prev = () => {
|
|
69
|
+
let page: any = {page: {limit, offset: offset - limit}};
|
|
70
|
+
return baseLink + queryParams(page, {encode: false});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add trailing pag links if not at the last page
|
|
75
|
+
if (total && (offset + limit < total)) {
|
|
76
|
+
obj.next = () => {
|
|
77
|
+
let page: any = {page: {limit, offset: offset + limit}};
|
|
78
|
+
return baseLink + queryParams(page, {encode: false});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
obj.last = () => {
|
|
82
|
+
// Avoiding overlapping with the penultimate page
|
|
83
|
+
let lastLimit: number = (total - (offset % limit)) % limit;
|
|
84
|
+
// If the limit fits perfectly in the total, reset it to the original
|
|
85
|
+
lastLimit = lastLimit === 0 ? limit : lastLimit;
|
|
86
|
+
|
|
87
|
+
let lastOffset: number = total - lastLimit;
|
|
88
|
+
let page: any = {page: {limit: lastLimit, offset: lastOffset }};
|
|
89
|
+
return baseLink + queryParams(page, {encode: false});
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return !isEmpty(obj) ? obj : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates links object for a resource
|
|
98
|
+
*/
|
|
99
|
+
export function dataLinks(linkOpts: LinkOpts): LinkObj {
|
|
100
|
+
let { baseUrl, type }: LinkOpts = linkOpts;
|
|
101
|
+
let baseLink: string = urlConcat(baseUrl, plural(type));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
self: function(resource: Model): string {
|
|
105
|
+
return urlConcat(baseLink, resource.id);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates links object for a relationship
|
|
112
|
+
*/
|
|
113
|
+
export function relationshipLinks(linkOpts: LinkOpts, related: string): LinkObj {
|
|
114
|
+
let { baseUrl, type }: LinkOpts = linkOpts;
|
|
115
|
+
let baseLink: string = urlConcat(baseUrl, plural(type));
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
self: function(resource: any, current: any, parent: Model): string {
|
|
119
|
+
return urlConcat(baseLink, parent.id, 'relationships', related);
|
|
120
|
+
},
|
|
121
|
+
related: function(resource: any, current: any, parent: Model): string {
|
|
122
|
+
return urlConcat(baseLink, parent.id, related);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates links object for a related resource, to be used for the included's array
|
|
129
|
+
*/
|
|
130
|
+
export function includedLinks(linkOpts: LinkOpts): LinkObj {
|
|
131
|
+
let { baseUrl, type }: LinkOpts = linkOpts;
|
|
132
|
+
let baseLink: string = urlConcat(baseUrl, plural(type));
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
self: function(primary: Model, current: Model): string {
|
|
136
|
+
return urlConcat(baseLink, current.id);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The main purpose of this module is to provide utility functions
|
|
3
|
+
* that follows the restrictions of the Bookshelf/Mapper/Serializer APIs
|
|
4
|
+
* with the goal of simplifying the logic of the main 'map' method.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assign,
|
|
9
|
+
clone,
|
|
10
|
+
cloneDeep,
|
|
11
|
+
filter,
|
|
12
|
+
includes,
|
|
13
|
+
intersection,
|
|
14
|
+
isArray,
|
|
15
|
+
isNil,
|
|
16
|
+
isString,
|
|
17
|
+
isUndefined,
|
|
18
|
+
escapeRegExp,
|
|
19
|
+
forOwn,
|
|
20
|
+
has,
|
|
21
|
+
keys,
|
|
22
|
+
map,
|
|
23
|
+
mapValues,
|
|
24
|
+
merge,
|
|
25
|
+
omit,
|
|
26
|
+
reduce,
|
|
27
|
+
some,
|
|
28
|
+
toString,
|
|
29
|
+
update
|
|
30
|
+
} from 'lodash';
|
|
31
|
+
|
|
32
|
+
import { LinkOpts, RelationOpts } from '../interfaces';
|
|
33
|
+
import { SerialOpts } from '../serializer';
|
|
34
|
+
import { AttrMatcher, AttributesOpt } from '../interfaces';
|
|
35
|
+
import { topLinks, dataLinks, relationshipLinks, includedLinks } from './links';
|
|
36
|
+
import { BookOpts, Data, Model, isModel, isCollection } from './extras';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Main structure used through most utility and recursive functions
|
|
40
|
+
*/
|
|
41
|
+
export interface Information {
|
|
42
|
+
bookOpts: BookOpts;
|
|
43
|
+
linkOpts: LinkOpts;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Start the data processing with top level information,
|
|
48
|
+
* then handle resources recursively in processSample
|
|
49
|
+
*/
|
|
50
|
+
export function processData(info: Information, data: Data): SerialOpts {
|
|
51
|
+
let { bookOpts: { enableLinks }, linkOpts }: Information = info;
|
|
52
|
+
|
|
53
|
+
let template: SerialOpts = processSample(info, sample(data));
|
|
54
|
+
|
|
55
|
+
if (enableLinks) {
|
|
56
|
+
template.dataLinks = dataLinks(linkOpts);
|
|
57
|
+
template.topLevelLinks = topLinks(linkOpts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return template;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively adds data-related properties to the
|
|
65
|
+
* template to be sent to the serializer
|
|
66
|
+
*/
|
|
67
|
+
function processSample(info: Information, sample: Sample): SerialOpts {
|
|
68
|
+
let { bookOpts, linkOpts }: Information = info;
|
|
69
|
+
let { enableLinks }: BookOpts = bookOpts;
|
|
70
|
+
|
|
71
|
+
let template: SerialOpts = {
|
|
72
|
+
// Add list of valid attributes
|
|
73
|
+
attributes: getAttrsList(sample, bookOpts)
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Nested relations (recursive) template generation
|
|
77
|
+
forOwn(sample.relations, (relSample: Sample, relName: string): void => {
|
|
78
|
+
if (!relationAllowed(bookOpts, relName)) { return; }
|
|
79
|
+
|
|
80
|
+
let relLinkOpts: LinkOpts = assign(clone(linkOpts), {type: relName});
|
|
81
|
+
let relTemplate: SerialOpts = processSample({bookOpts, linkOpts: relLinkOpts}, relSample);
|
|
82
|
+
relTemplate.ref = 'id'; // Add reference in nested resources
|
|
83
|
+
|
|
84
|
+
// Related links
|
|
85
|
+
if (enableLinks) {
|
|
86
|
+
relTemplate.relationshipLinks = relationshipLinks(linkOpts, relName);
|
|
87
|
+
relTemplate.includedLinks = includedLinks(relLinkOpts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Include links as compound document
|
|
91
|
+
if (!includeAllowed(bookOpts, relName)) {
|
|
92
|
+
relTemplate.included = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
template[relName] = relTemplate;
|
|
96
|
+
(template.attributes as string[]).push(relName);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return template;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Representation of a sample, a model with only models in the relations,
|
|
104
|
+
* no collections
|
|
105
|
+
*/
|
|
106
|
+
interface Sample extends Model {
|
|
107
|
+
relations: {
|
|
108
|
+
[relationName: string]: Sample
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert any data into a model representing
|
|
114
|
+
* a complete sample to be used in the template generation
|
|
115
|
+
*/
|
|
116
|
+
function sample(data: Data): Sample {
|
|
117
|
+
if (isModel(data)) {
|
|
118
|
+
// Override type because we will overwrite relations
|
|
119
|
+
const sampled: Sample = cloneDeep(omit(data, 'relations')) as Sample;
|
|
120
|
+
sampled.relations = mapValues(data.relations, sample);
|
|
121
|
+
|
|
122
|
+
return sampled;
|
|
123
|
+
} else if (isCollection(data)) {
|
|
124
|
+
const first: Model = data.first();
|
|
125
|
+
const rest: Model[] = (<Model[]><unknown> data).slice(1);
|
|
126
|
+
return reduce(rest, mergeSample, sample(first));
|
|
127
|
+
} else {
|
|
128
|
+
return {} as Sample;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge two models into a representation of both
|
|
134
|
+
*/
|
|
135
|
+
function mergeSample(main: Sample, toMerge: Model): Sample {
|
|
136
|
+
const sampled: Sample = sample(toMerge);
|
|
137
|
+
main.attributes = merge(main.attributes, sampled.attributes);
|
|
138
|
+
main.relations = merge(main.relations, sampled.relations);
|
|
139
|
+
return main;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function matches(matcher: AttrMatcher, str: string): boolean {
|
|
143
|
+
let reg: RegExp;
|
|
144
|
+
|
|
145
|
+
if (typeof matcher === 'string') {
|
|
146
|
+
reg = RegExp(`^${escapeRegExp(matcher)}$`);
|
|
147
|
+
} else {
|
|
148
|
+
reg = matcher;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return reg.test(str);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Retrieve model's attribute names
|
|
155
|
+
* following filtering rules
|
|
156
|
+
*/
|
|
157
|
+
function getAttrsList(data: Model, bookOpts: BookOpts): string[] {
|
|
158
|
+
let idAttr: undefined | string | string[] = data.idAttribute;
|
|
159
|
+
if (isString(idAttr)) {
|
|
160
|
+
idAttr = [ idAttr ];
|
|
161
|
+
} else if (isUndefined(idAttr)) {
|
|
162
|
+
idAttr = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let attrs: string[] = keys(data.attributes);
|
|
166
|
+
let outputVirtuals = data.outputVirtuals;
|
|
167
|
+
|
|
168
|
+
if (!isNil(bookOpts.outputVirtuals)) {
|
|
169
|
+
outputVirtuals = bookOpts.outputVirtuals
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (data.virtuals && outputVirtuals) {
|
|
173
|
+
attrs = attrs.concat(keys(data.virtuals))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let { attributes = { omit: idAttr } }: BookOpts = bookOpts;
|
|
177
|
+
|
|
178
|
+
// cast it to the object version of the option
|
|
179
|
+
if (attributes instanceof Array) {
|
|
180
|
+
attributes = { include : attributes };
|
|
181
|
+
}
|
|
182
|
+
let { omit, include }: AttributesOpt = attributes;
|
|
183
|
+
|
|
184
|
+
return filter(attrs, (attr: string) => {
|
|
185
|
+
let included: boolean = true;
|
|
186
|
+
let omitted: boolean = false;
|
|
187
|
+
|
|
188
|
+
if (include) {
|
|
189
|
+
included = some(include, (m: AttrMatcher) => matches(m, attr));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (omit) {
|
|
193
|
+
omitted = some(omit, (m: AttrMatcher) => matches(m, attr));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// `omit` has more precedence than `include` option
|
|
197
|
+
return ! omitted && included;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Based on Bookshelf options, determine if a relation must be included
|
|
203
|
+
*/
|
|
204
|
+
function relationAllowed(bookOpts: BookOpts, relName: string): boolean {
|
|
205
|
+
let { relations }: BookOpts = bookOpts;
|
|
206
|
+
|
|
207
|
+
if (typeof relations === 'boolean') {
|
|
208
|
+
return relations;
|
|
209
|
+
} else {
|
|
210
|
+
let { fields }: RelationOpts = relations;
|
|
211
|
+
return ! fields || includes(fields, relName);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Based on Bookshelf options, determine if a relation must be included
|
|
217
|
+
*/
|
|
218
|
+
function includeAllowed(bookOpts: BookOpts, relName: string): boolean {
|
|
219
|
+
let { relations }: BookOpts = bookOpts;
|
|
220
|
+
|
|
221
|
+
if (typeof relations === 'boolean') {
|
|
222
|
+
return relations;
|
|
223
|
+
} else {
|
|
224
|
+
let { fields, included }: RelationOpts = relations;
|
|
225
|
+
|
|
226
|
+
if (typeof included === 'boolean') {
|
|
227
|
+
return included;
|
|
228
|
+
} else {
|
|
229
|
+
// If included is an array, only allow relations that are in that array
|
|
230
|
+
let allowed: string[] = included;
|
|
231
|
+
|
|
232
|
+
if (fields) {
|
|
233
|
+
// If fields specified, ensure that the included relations
|
|
234
|
+
// are listed as one of the relations to be serialized
|
|
235
|
+
allowed = intersection(fields, included);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return includes(allowed, relName);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Convert a bookshelf model or collection to
|
|
245
|
+
* json adding the id attribute if missing
|
|
246
|
+
*/
|
|
247
|
+
export function toJSON(data: Data): any {
|
|
248
|
+
|
|
249
|
+
let json: any = null;
|
|
250
|
+
|
|
251
|
+
if (isModel(data)) {
|
|
252
|
+
json = data.toJSON({shallow: true}); // serialize without the relations
|
|
253
|
+
|
|
254
|
+
// When idAttribute is a composite id, calling .id returns `undefined`
|
|
255
|
+
const idAttr: undefined | string | string[] = data.idAttribute;
|
|
256
|
+
if (isArray(idAttr)) {
|
|
257
|
+
// the id will be the values in order separated by comma
|
|
258
|
+
data.id = map(idAttr, (attr: string) => data.attributes[attr]).join(',');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Assign the id for the model if it's not present already
|
|
262
|
+
if (! has(json, 'id')) {
|
|
263
|
+
json.id = data.id;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
update(json, 'id', toString);
|
|
267
|
+
|
|
268
|
+
// Loop over model relations to call toJSON recursively on them
|
|
269
|
+
forOwn(data.relations, function (relData: Data, relName: string): void {
|
|
270
|
+
json[relName] = toJSON(relData);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
} else if (isCollection(data)) {
|
|
274
|
+
// Run a recursive toJSON on each model of the collection
|
|
275
|
+
json = data.map(toJSON);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return json;
|
|
279
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PagOpts, QueryOpts } from './links';
|
|
2
|
+
import { RelationTypeOpt, RelationOpts } from './relations';
|
|
3
|
+
|
|
4
|
+
//// GENERAL INTERFACES FOR MAPPERS
|
|
5
|
+
|
|
6
|
+
// Mapper
|
|
7
|
+
export interface Mapper {
|
|
8
|
+
map(data: any, type: string, mapOpts?: MapOpts): any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AttrMatcher = RegExp | string;
|
|
12
|
+
|
|
13
|
+
export type AttributesOpt = {
|
|
14
|
+
omit?: AttrMatcher[],
|
|
15
|
+
include?: AttrMatcher[]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mapper Options
|
|
19
|
+
export interface MapOpts {
|
|
20
|
+
// Attributes-related
|
|
21
|
+
attributes?: AttrMatcher[] | AttributesOpt;
|
|
22
|
+
keyForAttr?: (attr: string) => string;
|
|
23
|
+
|
|
24
|
+
// Relations-related
|
|
25
|
+
relations?: boolean | RelationOpts;
|
|
26
|
+
typeForModel?: RelationTypeOpt;
|
|
27
|
+
|
|
28
|
+
// Links-related
|
|
29
|
+
enableLinks?: boolean;
|
|
30
|
+
pagination?: PagOpts;
|
|
31
|
+
query?: QueryOpts;
|
|
32
|
+
|
|
33
|
+
// Meta-related
|
|
34
|
+
meta?: { [key:string]: any };
|
|
35
|
+
|
|
36
|
+
// Virtuals-related
|
|
37
|
+
outputVirtuals?: boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Parameters map
|
|
3
|
+
*/
|
|
4
|
+
export interface QueryOpts {
|
|
5
|
+
[key: string]: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pagination variables
|
|
10
|
+
*/
|
|
11
|
+
export interface PagOpts {
|
|
12
|
+
offset: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
total?: number;
|
|
15
|
+
rowCount?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Data required to form links
|
|
20
|
+
*/
|
|
21
|
+
export interface LinkOpts {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
type: string;
|
|
24
|
+
pag?: PagOpts;
|
|
25
|
+
query?: QueryOpts;
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map to specify type for the passed relations
|
|
3
|
+
*/
|
|
4
|
+
export interface RelationTypeMap {
|
|
5
|
+
[relationName: string]: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Function to pass directly as the typeForAttributes option to the serializer
|
|
10
|
+
*/
|
|
11
|
+
export type RelationTypeFunction = (attribute: string) => string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The relationTypes option can be a function or an object
|
|
15
|
+
*/
|
|
16
|
+
export type RelationTypeOpt = RelationTypeMap | RelationTypeFunction;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Relationship options
|
|
20
|
+
*/
|
|
21
|
+
export interface RelationOpts {
|
|
22
|
+
included: boolean | string[];
|
|
23
|
+
fields?: string[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as jas from 'jsonapi-serializer';
|
|
2
|
+
|
|
3
|
+
export type LinkFunc = (primary: any, related?: any, parent?: any) => string;
|
|
4
|
+
|
|
5
|
+
export type Link = string | LinkFunc;
|
|
6
|
+
|
|
7
|
+
export interface LinkObj {
|
|
8
|
+
self?: Link;
|
|
9
|
+
related?: Link;
|
|
10
|
+
|
|
11
|
+
first?: Link;
|
|
12
|
+
last?: Link;
|
|
13
|
+
|
|
14
|
+
prev?: Link;
|
|
15
|
+
next?: Link;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SerialOpts {
|
|
19
|
+
attributes?: string[];
|
|
20
|
+
ref?: string;
|
|
21
|
+
included?: boolean;
|
|
22
|
+
|
|
23
|
+
topLevelLinks?: LinkObj;
|
|
24
|
+
dataLinks?: LinkObj;
|
|
25
|
+
relationshipLinks?: LinkObj;
|
|
26
|
+
includedLinks?: LinkObj;
|
|
27
|
+
|
|
28
|
+
relationshipMeta?: any;
|
|
29
|
+
ignoreRelationshipData?: boolean;
|
|
30
|
+
|
|
31
|
+
keyForAttribute?: (attribute: any) => string;
|
|
32
|
+
typeForAttribute?: (attribute: any) => any;
|
|
33
|
+
pluralizeType?: boolean;
|
|
34
|
+
|
|
35
|
+
meta?: any;
|
|
36
|
+
|
|
37
|
+
// TODO improve type-checking of relationship options
|
|
38
|
+
[relationships: string]: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SerializerCtor {
|
|
42
|
+
new(type: string, opts: SerialOpts): Serializer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Serializer {
|
|
46
|
+
serialize(data: any): any;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// tslint:disable-next-line variable-name
|
|
50
|
+
export let Serializer: SerializerCtor = jas.Serializer;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The purpose of this module is to extend the initially defined properties,
|
|
3
|
+
* behaviors and characteristics of the bookshelf API
|
|
4
|
+
*/
|
|
5
|
+
import { Model as BModel, Collection as BCollection } from 'bookshelf';
|
|
6
|
+
import { MapOpts, RelationTypeOpt, RelationOpts } from '../interfaces';
|
|
7
|
+
export interface BookOpts extends MapOpts {
|
|
8
|
+
keyForAttr: (attr: string) => string;
|
|
9
|
+
relations: boolean | RelationOpts;
|
|
10
|
+
typeForModel: RelationTypeOpt;
|
|
11
|
+
enableLinks: boolean;
|
|
12
|
+
outputVirtuals?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Internal form of the relations property of bookshelf objects
|
|
16
|
+
*/
|
|
17
|
+
export interface RelationsObject {
|
|
18
|
+
[relationName: string]: Data;
|
|
19
|
+
}
|
|
20
|
+
export interface Attributes {
|
|
21
|
+
[attrName: string]: any;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Bookshelf Model including some private properties
|
|
25
|
+
*/
|
|
26
|
+
export interface Model extends BModel<any> {
|
|
27
|
+
id: any;
|
|
28
|
+
idAttribute: any;
|
|
29
|
+
attributes: Attributes;
|
|
30
|
+
relations: RelationsObject;
|
|
31
|
+
virtuals?: any;
|
|
32
|
+
outputVirtuals?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Bookshelf Collection including some private properties
|
|
36
|
+
*/
|
|
37
|
+
export interface Collection extends BCollection<any> {
|
|
38
|
+
models: Model[];
|
|
39
|
+
length: number;
|
|
40
|
+
}
|
|
41
|
+
export type Data = Model | Collection;
|
|
42
|
+
/**
|
|
43
|
+
* Bookshelf Model Type Guard
|
|
44
|
+
* https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html
|
|
45
|
+
*/
|
|
46
|
+
export declare function isModel(data: Data): data is Model;
|
|
47
|
+
/**
|
|
48
|
+
* Bookshelf Collection Type Guard
|
|
49
|
+
* https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html
|
|
50
|
+
*/
|
|
51
|
+
export declare function isCollection(data: Data): data is Collection;
|
|
52
|
+
//# sourceMappingURL=extras.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extras.d.ts","sourceRoot":"","sources":["../../src/bookshelf/extras.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGvE,MAAM,WAAW,QAAS,SAAQ,OAAO;IAEvC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAGrC,SAAS,EAAE,OAAO,GAAG,YAAY,CAAC;IAClC,YAAY,EAAE,eAAe,CAAC;IAG9B,WAAW,EAAE,OAAO,CAAC;IAGrB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,KAAM,SAAQ,MAAM,CAAC,GAAG,CAAC;IACxC,EAAE,EAAE,GAAG,CAAC;IAIR,WAAW,EAAE,GAAG,CAAC;IAEjB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAW,SAAQ,WAAW,CAAC,GAAG,CAAC;IAClD,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,UAAU,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,KAAK,CAEjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,UAAU,CAG3D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SerialOpts } from '../serializer';
|
|
2
|
+
import { Mapper, MapOpts } from '../interfaces';
|
|
3
|
+
import { Data } from './extras';
|
|
4
|
+
/**
|
|
5
|
+
* Mapper class for Bookshelf sources
|
|
6
|
+
*/
|
|
7
|
+
export declare class Bookshelf implements Mapper {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
serialOpts?: SerialOpts | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Standard constructor
|
|
12
|
+
*/
|
|
13
|
+
constructor(baseUrl: string, serialOpts?: SerialOpts | undefined);
|
|
14
|
+
/**
|
|
15
|
+
* Maps bookshelf data to a JSON-API 1.0 compliant object
|
|
16
|
+
*
|
|
17
|
+
* The `any` type data source is set for typing compatibility, but must be removed if possible
|
|
18
|
+
* TODO fix data any type
|
|
19
|
+
*/
|
|
20
|
+
map(data: Data | any, type: string, mapOpts?: MapOpts): any;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bookshelf/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAc,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAY,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAY,MAAM,UAAU,CAAC;AAG1C;;GAEG;AACH,qBAAa,SAAU,YAAW,MAAM;IAKnB,OAAO,EAAE,MAAM;IAAS,UAAU,CAAC,EAAE,UAAU;IAHlE;;OAEG;gBACgB,OAAO,EAAE,MAAM,EAAS,UAAU,CAAC,EAAE,UAAU,YAAA;IAElE;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,OAAY,GAAG,GAAG;CAuChE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LinkOpts } from '../interfaces';
|
|
2
|
+
import { LinkObj } from '../serializer';
|
|
3
|
+
/**
|
|
4
|
+
* Creates top level links object, for primary data and pagination links.
|
|
5
|
+
*/
|
|
6
|
+
export declare function topLinks(linkOpts: LinkOpts): LinkObj;
|
|
7
|
+
/**
|
|
8
|
+
* Creates links object for a resource
|
|
9
|
+
*/
|
|
10
|
+
export declare function dataLinks(linkOpts: LinkOpts): LinkObj;
|
|
11
|
+
/**
|
|
12
|
+
* Creates links object for a relationship
|
|
13
|
+
*/
|
|
14
|
+
export declare function relationshipLinks(linkOpts: LinkOpts, related: string): LinkObj;
|
|
15
|
+
/**
|
|
16
|
+
* Creates links object for a related resource, to be used for the included's array
|
|
17
|
+
*/
|
|
18
|
+
export declare function includedLinks(linkOpts: LinkOpts): LinkObj;
|
|
19
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/bookshelf/links.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAMxC;;GAEG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAsBpD;AA0DD;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CASrD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAY9E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CASzD"}
|