@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,55 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { assign, identity } from "lodash";
|
|
5
|
+
import { pluralize as plural } from "inflection";
|
|
6
|
+
import { Serializer } from "../serializer";
|
|
7
|
+
import { processData, toJSON } from "./utils";
|
|
8
|
+
class Bookshelf {
|
|
9
|
+
/**
|
|
10
|
+
* Standard constructor
|
|
11
|
+
*/
|
|
12
|
+
constructor(baseUrl, serialOpts) {
|
|
13
|
+
__publicField(this, "baseUrl", baseUrl);
|
|
14
|
+
__publicField(this, "serialOpts", serialOpts);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps bookshelf data to a JSON-API 1.0 compliant object
|
|
18
|
+
*
|
|
19
|
+
* The `any` type data source is set for typing compatibility, but must be removed if possible
|
|
20
|
+
* TODO fix data any type
|
|
21
|
+
*/
|
|
22
|
+
map(data, type, mapOpts = {}) {
|
|
23
|
+
const {
|
|
24
|
+
attributes,
|
|
25
|
+
keyForAttr = identity,
|
|
26
|
+
relations = true,
|
|
27
|
+
typeForModel = (attr) => plural(attr),
|
|
28
|
+
enableLinks = true,
|
|
29
|
+
pagination,
|
|
30
|
+
query,
|
|
31
|
+
meta,
|
|
32
|
+
outputVirtuals
|
|
33
|
+
} = mapOpts;
|
|
34
|
+
const bookOpts = {
|
|
35
|
+
attributes,
|
|
36
|
+
keyForAttr,
|
|
37
|
+
relations,
|
|
38
|
+
typeForModel,
|
|
39
|
+
enableLinks,
|
|
40
|
+
pagination,
|
|
41
|
+
query,
|
|
42
|
+
outputVirtuals
|
|
43
|
+
};
|
|
44
|
+
const linkOpts = { baseUrl: this.baseUrl, type, pag: pagination };
|
|
45
|
+
const info = { bookOpts, linkOpts };
|
|
46
|
+
const template = processData(info, data);
|
|
47
|
+
const typeForAttribute = typeof typeForModel === "function" ? typeForModel : (attr) => typeForModel[attr] || plural(attr);
|
|
48
|
+
assign(template, { typeForAttribute, keyForAttribute: keyForAttr, meta }, this.serialOpts);
|
|
49
|
+
const json = toJSON(data);
|
|
50
|
+
return new Serializer(type, template).serialize(json);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
Bookshelf
|
|
55
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { assign, omit, isEmpty, isNil } from "lodash";
|
|
2
|
+
import { pluralize as plural } from "inflection";
|
|
3
|
+
import { stringify as queryParams } from "qs";
|
|
4
|
+
function urlConcat(...parts) {
|
|
5
|
+
return parts.join("/");
|
|
6
|
+
}
|
|
7
|
+
function topLinks(linkOpts) {
|
|
8
|
+
let { baseUrl, type, pag } = linkOpts;
|
|
9
|
+
let obj = {
|
|
10
|
+
self: urlConcat(baseUrl, plural(type))
|
|
11
|
+
};
|
|
12
|
+
if (!isNil(pag)) {
|
|
13
|
+
if (!isNil(pag.rowCount)) {
|
|
14
|
+
pag.total = pag.rowCount;
|
|
15
|
+
}
|
|
16
|
+
if (!isNil(pag.total) && pag.total > 0 && pag.total > pag.limit) {
|
|
17
|
+
assign(obj, pagLinks(linkOpts));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
function pagLinks(linkOpts) {
|
|
23
|
+
let { baseUrl, type, pag, query = {} } = linkOpts;
|
|
24
|
+
if (pag === void 0) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
const { offset, limit, total } = pag;
|
|
28
|
+
let baseLink = urlConcat(baseUrl, plural(type));
|
|
29
|
+
query = omit(query, ["page", "page[limit]", "page[offset]"]);
|
|
30
|
+
baseLink = baseLink + "?" + queryParams(query, { encode: false });
|
|
31
|
+
let obj = {};
|
|
32
|
+
if (offset > 0) {
|
|
33
|
+
obj.first = () => {
|
|
34
|
+
let page = { page: { limit, offset: 0 } };
|
|
35
|
+
return baseLink + queryParams(page, { encode: false });
|
|
36
|
+
};
|
|
37
|
+
obj.prev = () => {
|
|
38
|
+
let page = { page: { limit, offset: offset - limit } };
|
|
39
|
+
return baseLink + queryParams(page, { encode: false });
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (total && offset + limit < total) {
|
|
43
|
+
obj.next = () => {
|
|
44
|
+
let page = { page: { limit, offset: offset + limit } };
|
|
45
|
+
return baseLink + queryParams(page, { encode: false });
|
|
46
|
+
};
|
|
47
|
+
obj.last = () => {
|
|
48
|
+
let lastLimit = (total - offset % limit) % limit;
|
|
49
|
+
lastLimit = lastLimit === 0 ? limit : lastLimit;
|
|
50
|
+
let lastOffset = total - lastLimit;
|
|
51
|
+
let page = { page: { limit: lastLimit, offset: lastOffset } };
|
|
52
|
+
return baseLink + queryParams(page, { encode: false });
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return !isEmpty(obj) ? obj : void 0;
|
|
56
|
+
}
|
|
57
|
+
function dataLinks(linkOpts) {
|
|
58
|
+
let { baseUrl, type } = linkOpts;
|
|
59
|
+
let baseLink = urlConcat(baseUrl, plural(type));
|
|
60
|
+
return {
|
|
61
|
+
self: function(resource) {
|
|
62
|
+
return urlConcat(baseLink, resource.id);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function relationshipLinks(linkOpts, related) {
|
|
67
|
+
let { baseUrl, type } = linkOpts;
|
|
68
|
+
let baseLink = urlConcat(baseUrl, plural(type));
|
|
69
|
+
return {
|
|
70
|
+
self: function(resource, current, parent) {
|
|
71
|
+
return urlConcat(baseLink, parent.id, "relationships", related);
|
|
72
|
+
},
|
|
73
|
+
related: function(resource, current, parent) {
|
|
74
|
+
return urlConcat(baseLink, parent.id, related);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function includedLinks(linkOpts) {
|
|
79
|
+
let { baseUrl, type } = linkOpts;
|
|
80
|
+
let baseLink = urlConcat(baseUrl, plural(type));
|
|
81
|
+
return {
|
|
82
|
+
self: function(primary, current) {
|
|
83
|
+
return urlConcat(baseLink, current.id);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
dataLinks,
|
|
89
|
+
includedLinks,
|
|
90
|
+
relationshipLinks,
|
|
91
|
+
topLinks
|
|
92
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assign,
|
|
3
|
+
clone,
|
|
4
|
+
cloneDeep,
|
|
5
|
+
filter,
|
|
6
|
+
includes,
|
|
7
|
+
intersection,
|
|
8
|
+
isArray,
|
|
9
|
+
isNil,
|
|
10
|
+
isString,
|
|
11
|
+
isUndefined,
|
|
12
|
+
escapeRegExp,
|
|
13
|
+
forOwn,
|
|
14
|
+
has,
|
|
15
|
+
keys,
|
|
16
|
+
map,
|
|
17
|
+
mapValues,
|
|
18
|
+
merge,
|
|
19
|
+
omit,
|
|
20
|
+
reduce,
|
|
21
|
+
some,
|
|
22
|
+
toString,
|
|
23
|
+
update
|
|
24
|
+
} from "lodash";
|
|
25
|
+
import { topLinks, dataLinks, relationshipLinks, includedLinks } from "./links";
|
|
26
|
+
import { isModel, isCollection } from "./extras";
|
|
27
|
+
function processData(info, data) {
|
|
28
|
+
let { bookOpts: { enableLinks }, linkOpts } = info;
|
|
29
|
+
let template = processSample(info, sample(data));
|
|
30
|
+
if (enableLinks) {
|
|
31
|
+
template.dataLinks = dataLinks(linkOpts);
|
|
32
|
+
template.topLevelLinks = topLinks(linkOpts);
|
|
33
|
+
}
|
|
34
|
+
return template;
|
|
35
|
+
}
|
|
36
|
+
function processSample(info, sample2) {
|
|
37
|
+
let { bookOpts, linkOpts } = info;
|
|
38
|
+
let { enableLinks } = bookOpts;
|
|
39
|
+
let template = {
|
|
40
|
+
// Add list of valid attributes
|
|
41
|
+
attributes: getAttrsList(sample2, bookOpts)
|
|
42
|
+
};
|
|
43
|
+
forOwn(sample2.relations, (relSample, relName) => {
|
|
44
|
+
if (!relationAllowed(bookOpts, relName)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let relLinkOpts = assign(clone(linkOpts), { type: relName });
|
|
48
|
+
let relTemplate = processSample({ bookOpts, linkOpts: relLinkOpts }, relSample);
|
|
49
|
+
relTemplate.ref = "id";
|
|
50
|
+
if (enableLinks) {
|
|
51
|
+
relTemplate.relationshipLinks = relationshipLinks(linkOpts, relName);
|
|
52
|
+
relTemplate.includedLinks = includedLinks(relLinkOpts);
|
|
53
|
+
}
|
|
54
|
+
if (!includeAllowed(bookOpts, relName)) {
|
|
55
|
+
relTemplate.included = false;
|
|
56
|
+
}
|
|
57
|
+
template[relName] = relTemplate;
|
|
58
|
+
template.attributes.push(relName);
|
|
59
|
+
});
|
|
60
|
+
return template;
|
|
61
|
+
}
|
|
62
|
+
function sample(data) {
|
|
63
|
+
if (isModel(data)) {
|
|
64
|
+
const sampled = cloneDeep(omit(data, "relations"));
|
|
65
|
+
sampled.relations = mapValues(data.relations, sample);
|
|
66
|
+
return sampled;
|
|
67
|
+
} else if (isCollection(data)) {
|
|
68
|
+
const first = data.first();
|
|
69
|
+
const rest = data.slice(1);
|
|
70
|
+
return reduce(rest, mergeSample, sample(first));
|
|
71
|
+
} else {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function mergeSample(main, toMerge) {
|
|
76
|
+
const sampled = sample(toMerge);
|
|
77
|
+
main.attributes = merge(main.attributes, sampled.attributes);
|
|
78
|
+
main.relations = merge(main.relations, sampled.relations);
|
|
79
|
+
return main;
|
|
80
|
+
}
|
|
81
|
+
function matches(matcher, str) {
|
|
82
|
+
let reg;
|
|
83
|
+
if (typeof matcher === "string") {
|
|
84
|
+
reg = RegExp(`^${escapeRegExp(matcher)}$`);
|
|
85
|
+
} else {
|
|
86
|
+
reg = matcher;
|
|
87
|
+
}
|
|
88
|
+
return reg.test(str);
|
|
89
|
+
}
|
|
90
|
+
function getAttrsList(data, bookOpts) {
|
|
91
|
+
let idAttr = data.idAttribute;
|
|
92
|
+
if (isString(idAttr)) {
|
|
93
|
+
idAttr = [idAttr];
|
|
94
|
+
} else if (isUndefined(idAttr)) {
|
|
95
|
+
idAttr = [];
|
|
96
|
+
}
|
|
97
|
+
let attrs = keys(data.attributes);
|
|
98
|
+
let outputVirtuals = data.outputVirtuals;
|
|
99
|
+
if (!isNil(bookOpts.outputVirtuals)) {
|
|
100
|
+
outputVirtuals = bookOpts.outputVirtuals;
|
|
101
|
+
}
|
|
102
|
+
if (data.virtuals && outputVirtuals) {
|
|
103
|
+
attrs = attrs.concat(keys(data.virtuals));
|
|
104
|
+
}
|
|
105
|
+
let { attributes = { omit: idAttr } } = bookOpts;
|
|
106
|
+
if (attributes instanceof Array) {
|
|
107
|
+
attributes = { include: attributes };
|
|
108
|
+
}
|
|
109
|
+
let { omit: omit2, include } = attributes;
|
|
110
|
+
return filter(attrs, (attr) => {
|
|
111
|
+
let included = true;
|
|
112
|
+
let omitted = false;
|
|
113
|
+
if (include) {
|
|
114
|
+
included = some(include, (m) => matches(m, attr));
|
|
115
|
+
}
|
|
116
|
+
if (omit2) {
|
|
117
|
+
omitted = some(omit2, (m) => matches(m, attr));
|
|
118
|
+
}
|
|
119
|
+
return !omitted && included;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function relationAllowed(bookOpts, relName) {
|
|
123
|
+
let { relations } = bookOpts;
|
|
124
|
+
if (typeof relations === "boolean") {
|
|
125
|
+
return relations;
|
|
126
|
+
} else {
|
|
127
|
+
let { fields } = relations;
|
|
128
|
+
return !fields || includes(fields, relName);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function includeAllowed(bookOpts, relName) {
|
|
132
|
+
let { relations } = bookOpts;
|
|
133
|
+
if (typeof relations === "boolean") {
|
|
134
|
+
return relations;
|
|
135
|
+
} else {
|
|
136
|
+
let { fields, included } = relations;
|
|
137
|
+
if (typeof included === "boolean") {
|
|
138
|
+
return included;
|
|
139
|
+
} else {
|
|
140
|
+
let allowed = included;
|
|
141
|
+
if (fields) {
|
|
142
|
+
allowed = intersection(fields, included);
|
|
143
|
+
}
|
|
144
|
+
return includes(allowed, relName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function toJSON(data) {
|
|
149
|
+
let json = null;
|
|
150
|
+
if (isModel(data)) {
|
|
151
|
+
json = data.toJSON({ shallow: true });
|
|
152
|
+
const idAttr = data.idAttribute;
|
|
153
|
+
if (isArray(idAttr)) {
|
|
154
|
+
data.id = map(idAttr, (attr) => data.attributes[attr]).join(",");
|
|
155
|
+
}
|
|
156
|
+
if (!has(json, "id")) {
|
|
157
|
+
json.id = data.id;
|
|
158
|
+
}
|
|
159
|
+
update(json, "id", toString);
|
|
160
|
+
forOwn(data.relations, function(relData, relName) {
|
|
161
|
+
json[relName] = toJSON(relData);
|
|
162
|
+
});
|
|
163
|
+
} else if (isCollection(data)) {
|
|
164
|
+
json = data.map(toJSON);
|
|
165
|
+
}
|
|
166
|
+
return json;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
processData,
|
|
170
|
+
toJSON
|
|
171
|
+
};
|
package/es/index.js
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tryghost/jsonapi-mapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JSON API-Compliant Serialization for your ORM",
|
|
5
|
+
"source": "src/index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"cjs",
|
|
8
|
+
"es",
|
|
9
|
+
"types",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"main": "cjs/index.js",
|
|
13
|
+
"module": "es/index.js",
|
|
14
|
+
"types": "types/index.d.ts",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "ssh://git@github.com/TryGhost/Pro-Packages",
|
|
18
|
+
"directory": "packages/jsonapi-mapper"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"json",
|
|
22
|
+
"api",
|
|
23
|
+
"json-api",
|
|
24
|
+
"jsonapi",
|
|
25
|
+
"orm",
|
|
26
|
+
"mapper",
|
|
27
|
+
"bookshelf",
|
|
28
|
+
"serializer",
|
|
29
|
+
"serialization"
|
|
30
|
+
],
|
|
31
|
+
"author": "James Dixon <jim.w.dixon@gmail.com>",
|
|
32
|
+
"contributors": [
|
|
33
|
+
{
|
|
34
|
+
"name": "Manuel Pacheco",
|
|
35
|
+
"email": "manuelalejandropm@gmail.com"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "Matteo Ferrando",
|
|
39
|
+
"email": "matteo.ferrando2@gmail.com"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/TryGhost/Pro-Packages/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/TryGhost/Pro-Packages/tree/main/packages/jsonapi-mapper",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"bookshelf": "1.2.0",
|
|
49
|
+
"inflection": "1.12.0",
|
|
50
|
+
"jsonapi-serializer": "3.5.6",
|
|
51
|
+
"knex": "0.21.1",
|
|
52
|
+
"lodash": "^4.17.20",
|
|
53
|
+
"qs": "6.5.1",
|
|
54
|
+
"type-check": "0.3.2"
|
|
55
|
+
},
|
|
56
|
+
"nx": {
|
|
57
|
+
"targets": {
|
|
58
|
+
"build": {
|
|
59
|
+
"outputs": [
|
|
60
|
+
"{projectRoot}/cjs",
|
|
61
|
+
"{projectRoot}/es",
|
|
62
|
+
"{projectRoot}/types"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/bookshelf": "1.1.1",
|
|
69
|
+
"@types/inflection": "1.5.28",
|
|
70
|
+
"@types/knex": "0.0.61",
|
|
71
|
+
"@types/lodash": "4.14.93",
|
|
72
|
+
"@types/node": "25.7.0",
|
|
73
|
+
"@types/qs": "6.5.1",
|
|
74
|
+
"esbuild": "0.28.0",
|
|
75
|
+
"typescript": "5.9.3"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"dev": "echo \"Implement me!\"",
|
|
79
|
+
"pretest": "NODE_ENV=production pnpm run build",
|
|
80
|
+
"build": "pnpm run build:cjs && pnpm run build:es && pnpm run build:types",
|
|
81
|
+
"build:cjs": "esbuild src/index.ts 'src/**/*.ts' --target=es2020 --outdir=cjs --format=cjs",
|
|
82
|
+
"build:es": "esbuild src/index.ts 'src/**/*.ts' --target=es2020 --outdir=es --format=esm",
|
|
83
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir types",
|
|
84
|
+
"test": "NODE_ENV=testing vitest run --coverage",
|
|
85
|
+
"lint": "oxlint -c ../../.oxlintrc.json .",
|
|
86
|
+
"posttest": "pnpm run lint"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The purpose of this module is to extend the initially defined properties,
|
|
3
|
+
* behaviors and characteristics of the bookshelf API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Model as BModel, Collection as BCollection } from 'bookshelf';
|
|
7
|
+
import { MapOpts, RelationTypeOpt, RelationOpts } from '../interfaces';
|
|
8
|
+
|
|
9
|
+
// Bookshelf Options
|
|
10
|
+
export interface BookOpts extends MapOpts {
|
|
11
|
+
// Attributes-related
|
|
12
|
+
keyForAttr: (attr: string) => string;
|
|
13
|
+
|
|
14
|
+
// Relations-related
|
|
15
|
+
relations: boolean | RelationOpts;
|
|
16
|
+
typeForModel: RelationTypeOpt;
|
|
17
|
+
|
|
18
|
+
// Links-related
|
|
19
|
+
enableLinks: boolean;
|
|
20
|
+
|
|
21
|
+
// Virtuals-related;
|
|
22
|
+
outputVirtuals?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal form of the relations property of bookshelf objects
|
|
27
|
+
*/
|
|
28
|
+
export interface RelationsObject {
|
|
29
|
+
[relationName: string]: Data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Attributes {
|
|
33
|
+
[attrName: string]: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bookshelf Model including some private properties
|
|
38
|
+
*/
|
|
39
|
+
export interface Model extends BModel<any> {
|
|
40
|
+
id: any;
|
|
41
|
+
|
|
42
|
+
// TODO: PR to fix Bookshelf types
|
|
43
|
+
// idAttribute?: string | string[];
|
|
44
|
+
idAttribute: any;
|
|
45
|
+
|
|
46
|
+
attributes: Attributes;
|
|
47
|
+
relations: RelationsObject;
|
|
48
|
+
virtuals?: any;
|
|
49
|
+
outputVirtuals?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bookshelf Collection including some private properties
|
|
54
|
+
*/
|
|
55
|
+
export interface Collection extends BCollection<any> {
|
|
56
|
+
models: Model[];
|
|
57
|
+
length: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Data = Model | Collection;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bookshelf Model Type Guard
|
|
64
|
+
* https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html
|
|
65
|
+
*/
|
|
66
|
+
export function isModel(data: Data): data is Model {
|
|
67
|
+
return data ? !isCollection(data) : false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Bookshelf Collection Type Guard
|
|
72
|
+
* https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html
|
|
73
|
+
*/
|
|
74
|
+
export function isCollection(data: Data): data is Collection {
|
|
75
|
+
// Type recognition based on duck-typing
|
|
76
|
+
return data ? (data as Collection).models !== undefined : false;
|
|
77
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { assign, identity } from 'lodash';
|
|
2
|
+
import { pluralize as plural } from 'inflection';
|
|
3
|
+
|
|
4
|
+
import { SerialOpts, Serializer } from '../serializer';
|
|
5
|
+
import { Mapper, MapOpts, LinkOpts } from '../interfaces';
|
|
6
|
+
import { Data, BookOpts } from './extras';
|
|
7
|
+
import { Information, processData, toJSON } from './utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mapper class for Bookshelf sources
|
|
11
|
+
*/
|
|
12
|
+
export class Bookshelf implements Mapper {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Standard constructor
|
|
16
|
+
*/
|
|
17
|
+
constructor(public baseUrl: string, public serialOpts?: SerialOpts) { }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maps bookshelf data to a JSON-API 1.0 compliant object
|
|
21
|
+
*
|
|
22
|
+
* The `any` type data source is set for typing compatibility, but must be removed if possible
|
|
23
|
+
* TODO fix data any type
|
|
24
|
+
*/
|
|
25
|
+
map(data: Data | any, type: string, mapOpts: MapOpts = {}): any {
|
|
26
|
+
|
|
27
|
+
// Set default values for the options
|
|
28
|
+
const {
|
|
29
|
+
attributes,
|
|
30
|
+
keyForAttr = identity,
|
|
31
|
+
relations = true,
|
|
32
|
+
typeForModel = (attr: string) => plural(attr),
|
|
33
|
+
enableLinks = true,
|
|
34
|
+
pagination,
|
|
35
|
+
query,
|
|
36
|
+
meta,
|
|
37
|
+
outputVirtuals
|
|
38
|
+
}: MapOpts = mapOpts;
|
|
39
|
+
|
|
40
|
+
const bookOpts: BookOpts = {
|
|
41
|
+
attributes, keyForAttr,
|
|
42
|
+
relations, typeForModel,
|
|
43
|
+
enableLinks, pagination,
|
|
44
|
+
query, outputVirtuals
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const linkOpts: LinkOpts = { baseUrl: this.baseUrl, type, pag: pagination };
|
|
48
|
+
|
|
49
|
+
const info: Information = { bookOpts, linkOpts };
|
|
50
|
+
const template: SerialOpts = processData(info, data);
|
|
51
|
+
|
|
52
|
+
const typeForAttribute: (attr: string) => string =
|
|
53
|
+
typeof typeForModel === 'function'
|
|
54
|
+
? typeForModel
|
|
55
|
+
: (attr: string) => typeForModel[attr] || plural(attr); // pluralize when falsy
|
|
56
|
+
|
|
57
|
+
// Override the template with the provided serializer options
|
|
58
|
+
assign(template, { typeForAttribute, keyForAttribute: keyForAttr, meta }, this.serialOpts);
|
|
59
|
+
|
|
60
|
+
// Return the data in JSON API format
|
|
61
|
+
const json: any = toJSON(data);
|
|
62
|
+
return new Serializer(type, template).serialize(json);
|
|
63
|
+
}
|
|
64
|
+
}
|