@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.
Files changed (56) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +103 -0
  3. package/cjs/bookshelf/extras.js +30 -0
  4. package/cjs/bookshelf/index.js +74 -0
  5. package/cjs/bookshelf/links.js +112 -0
  6. package/cjs/bookshelf/utils.js +168 -0
  7. package/cjs/index.js +20 -0
  8. package/cjs/interfaces/common.js +16 -0
  9. package/cjs/interfaces/index.js +20 -0
  10. package/cjs/interfaces/links.js +16 -0
  11. package/cjs/interfaces/relations.js +16 -0
  12. package/cjs/serializer/index.js +35 -0
  13. package/cjs/serializer/jsonapi-serializer.skel.d.js +1 -0
  14. package/es/bookshelf/extras.js +10 -0
  15. package/es/bookshelf/index.js +55 -0
  16. package/es/bookshelf/links.js +92 -0
  17. package/es/bookshelf/utils.js +171 -0
  18. package/es/index.js +3 -0
  19. package/es/interfaces/common.js +0 -0
  20. package/es/interfaces/index.js +3 -0
  21. package/es/interfaces/links.js +0 -0
  22. package/es/interfaces/relations.js +0 -0
  23. package/es/serializer/index.js +5 -0
  24. package/es/serializer/jsonapi-serializer.skel.d.js +0 -0
  25. package/package.json +88 -0
  26. package/src/bookshelf/extras.ts +77 -0
  27. package/src/bookshelf/index.ts +64 -0
  28. package/src/bookshelf/links.ts +139 -0
  29. package/src/bookshelf/utils.ts +279 -0
  30. package/src/index.ts +3 -0
  31. package/src/interfaces/common.ts +38 -0
  32. package/src/interfaces/index.ts +3 -0
  33. package/src/interfaces/links.ts +26 -0
  34. package/src/interfaces/relations.ts +24 -0
  35. package/src/serializer/index.ts +50 -0
  36. package/src/serializer/jsonapi-serializer.skel.d.ts +4 -0
  37. package/types/bookshelf/extras.d.ts +52 -0
  38. package/types/bookshelf/extras.d.ts.map +1 -0
  39. package/types/bookshelf/index.d.ts +22 -0
  40. package/types/bookshelf/index.d.ts.map +1 -0
  41. package/types/bookshelf/links.d.ts +19 -0
  42. package/types/bookshelf/links.d.ts.map +1 -0
  43. package/types/bookshelf/utils.d.ts +26 -0
  44. package/types/bookshelf/utils.d.ts.map +1 -0
  45. package/types/index.d.ts +4 -0
  46. package/types/index.d.ts.map +1 -0
  47. package/types/interfaces/common.d.ts +24 -0
  48. package/types/interfaces/common.d.ts.map +1 -0
  49. package/types/interfaces/index.d.ts +4 -0
  50. package/types/interfaces/index.d.ts.map +1 -0
  51. package/types/interfaces/links.d.ts +25 -0
  52. package/types/interfaces/links.d.ts.map +1 -0
  53. package/types/interfaces/relations.d.ts +22 -0
  54. package/types/interfaces/relations.d.ts.map +1 -0
  55. package/types/serializer/index.d.ts +34 -0
  56. 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
@@ -0,0 +1,3 @@
1
+ export * from "./bookshelf";
2
+ export * from "./interfaces";
3
+ export * from "./serializer";
File without changes
@@ -0,0 +1,3 @@
1
+ export * from "./common";
2
+ export * from "./links";
3
+ export * from "./relations";
File without changes
File without changes
@@ -0,0 +1,5 @@
1
+ import * as jas from "jsonapi-serializer";
2
+ let Serializer = jas.Serializer;
3
+ export {
4
+ Serializer
5
+ };
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
+ }