@clipboard-health/json-api 0.1.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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @clipboard-health/json-api
2
+
3
+ Utilities for adhering to the [JSON:API](https://jsonapi.org/) specification.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Install](#install)
8
+ - [Usage](#usage)
9
+ - [Local development commands](#local-development-commands)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @clipboard-health/json-api
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Query helpers
20
+
21
+ From the client, call `toSearchParams` to convert from `JsonApiQuery` to `URLSearchParams`:
22
+
23
+ <!-- prettier-ignore -->
24
+ ```ts
25
+ // ./examples/toSearchParams.ts
26
+
27
+ import { toSearchParams } from "@clipboard-health/json-api";
28
+
29
+ import { type JsonApiQuery } from "../src/lib/types";
30
+
31
+ const query: JsonApiQuery = {
32
+ fields: { dog: ["age", "name"] },
33
+ filter: { age: ["2", "5"] },
34
+ include: ["vet"],
35
+ page: { size: "10" },
36
+ sort: ["-age"],
37
+ };
38
+
39
+ console.log(toSearchParams(query).toString());
40
+ // Note: actual result is URL-encoded, but unencoded below for readability
41
+ // => fields[dog]=age,name&filter[age]=2,5&include=vet&page[size]=10&sort=-age
42
+
43
+ ```
44
+
45
+ From the server, call `toJsonApiQuery` to convert from `URLSearchParams` to `JsonApiQuery`:
46
+
47
+ <!-- prettier-ignore -->
48
+ ```ts
49
+ // ./examples/toJsonApiQuery.ts
50
+
51
+ import { toJsonApiQuery } from "@clipboard-health/json-api";
52
+
53
+ const searchParams = new URLSearchParams(
54
+ "fields%5Bdog%5D=age%2Cname&filter%5Bage%5D=2%2C5&include=vet&page%5Bsize%5D=10&sort=-age",
55
+ );
56
+
57
+ console.log(toJsonApiQuery(searchParams));
58
+ // => {
59
+ // fields: { dog: ["age", "name"] },
60
+ // filter: { age: ["2", "5"] },
61
+ // include: ["vet"],
62
+ // page: { size: "10" },
63
+ // sort: ["-age"],
64
+ // }
65
+
66
+ ```
67
+
68
+ ## Local development commands
69
+
70
+ See [`package.json`](./package.json) `scripts` for a list of commands.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@clipboard-health/json-api",
3
+ "description": "",
4
+ "version": "0.1.0",
5
+ "bugs": "https://github.com/clipboardhealth/core-utils/issues",
6
+ "dependencies": {
7
+ "tslib": "2.6.3"
8
+ },
9
+ "keywords": [],
10
+ "license": "MIT",
11
+ "main": "./src/index.js",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/clipboardhealth/core-utils.git",
18
+ "directory": "packages/json-api"
19
+ },
20
+ "scripts": {
21
+ "embed": "embedme README.md"
22
+ },
23
+ "type": "commonjs",
24
+ "typings": "./src/index.d.ts",
25
+ "types": "./src/index.d.ts"
26
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./lib/toJsonApiQuery";
2
+ export * from "./lib/toSearchParams";
3
+ export * from "./lib/types";
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/toJsonApiQuery"), exports);
5
+ tslib_1.__exportStar(require("./lib/toSearchParams"), exports);
6
+ tslib_1.__exportStar(require("./lib/types"), exports);
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/json-api/src/index.ts"],"names":[],"mappings":";;;AAAA,+DAAqC;AACrC,+DAAqC;AACrC,sDAA4B"}
@@ -0,0 +1,7 @@
1
+ import { type JsonApiQuery } from "./types";
2
+ /**
3
+ * Call this function from clients to convert from {@link URLSearchParams} to {@link JsonApiQuery}.
4
+ *
5
+ * @see [Example](https://github.com/ClipboardHealth/core-utils/blob/main/packages/json-api/examples/toJsonApiQuery.ts)
6
+ */
7
+ export declare function toJsonApiQuery(searchParams: URLSearchParams): JsonApiQuery;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toJsonApiQuery = toJsonApiQuery;
4
+ const REGEX = {
5
+ fields: /^fields\[(.*?)]$/i,
6
+ filter: /^filter\[([^\]]*?)]$/i,
7
+ filterType: /^filter\[(.*?)]\[(.*?)]$/i,
8
+ include: /^include$/i,
9
+ page: /^page\[(.*?)]$/i,
10
+ sort: /^sort$/i,
11
+ };
12
+ /**
13
+ * Call this function from clients to convert from {@link URLSearchParams} to {@link JsonApiQuery}.
14
+ *
15
+ * @see [Example](https://github.com/ClipboardHealth/core-utils/blob/main/packages/json-api/examples/toJsonApiQuery.ts)
16
+ */
17
+ function toJsonApiQuery(searchParams) {
18
+ return [...searchParams].reduce((accumulator, [key, value]) => {
19
+ const match = Object.entries(REGEX).find(([, regex]) => regex.test(key));
20
+ if (!match) {
21
+ return accumulator;
22
+ }
23
+ const [type, regex] = match;
24
+ const groups = regex.exec(key)?.slice(1);
25
+ if (type === "fields" && groups?.[0]) {
26
+ return {
27
+ ...accumulator,
28
+ fields: {
29
+ ...accumulator.fields,
30
+ [groups[0]]: value.split(","),
31
+ },
32
+ };
33
+ }
34
+ if ((type === "filter" || type === "filterType") && groups?.length) {
35
+ const [field, fieldType] = groups;
36
+ if (field) {
37
+ return {
38
+ ...accumulator,
39
+ filter: {
40
+ ...accumulator.filter,
41
+ [field]: fieldType
42
+ ? { ...accumulator.filter?.[field], [fieldType]: value }
43
+ : value.split(","),
44
+ },
45
+ };
46
+ }
47
+ }
48
+ if (type === "include" || type === "sort") {
49
+ return { ...accumulator, [type]: value.split(",") };
50
+ }
51
+ if (type === "page" && groups?.[0]) {
52
+ return {
53
+ ...accumulator,
54
+ page: { ...accumulator.page, [groups[0]]: value },
55
+ };
56
+ }
57
+ /* istanbul ignore next */
58
+ return accumulator;
59
+ }, {});
60
+ }
61
+ //# sourceMappingURL=toJsonApiQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toJsonApiQuery.js","sourceRoot":"","sources":["../../../../../packages/json-api/src/lib/toJsonApiQuery.ts"],"names":[],"mappings":";;AAgBA,wCAgDC;AA9DD,MAAM,KAAK,GAAG;IACZ,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,uBAAuB;IAC/B,UAAU,EAAE,2BAA2B;IACvC,OAAO,EAAE,YAAY;IACrB,IAAI,EAAE,iBAAiB;IACvB,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;;;GAIG;AACH,SAAgB,cAAc,CAAC,YAA6B;IAC1D,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC,MAAM,CAAe,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,KAAqC,CAAC;QAC5D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,OAAO;gBACL,GAAG,WAAW;gBACd,MAAM,EAAE;oBACN,GAAG,WAAW,CAAC,MAAM;oBACrB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;iBAC9B;aACF,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,YAAY,CAAC,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC;YACnE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC;YAClC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO;oBACL,GAAG,WAAW;oBACd,MAAM,EAAE;wBACN,GAAG,WAAW,CAAC,MAAM;wBACrB,CAAC,KAAK,CAAC,EAAE,SAAS;4BAChB,CAAC,CAAC,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE;4BACxD,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;qBACrB;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;QAED,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1C,OAAO,EAAE,GAAG,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtD,CAAC;QAED,IAAI,IAAI,KAAK,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnC,OAAO;gBACL,GAAG,WAAW;gBACd,IAAI,EAAE,EAAE,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE;aAClD,CAAC;QACJ,CAAC;QAED,0BAA0B;QAC1B,OAAO,WAAW,CAAC;IACrB,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { URLSearchParams } from "node:url";
2
+ import { type JsonApiQuery } from "./types";
3
+ /**
4
+ * Call this function from clients to convert from {@link JsonApiQuery} to {@link URLSearchParams}.
5
+ *
6
+ * @see [Example](https://github.com/ClipboardHealth/core-utils/blob/main/packages/json-api/examples/toSearchParams.ts)
7
+ */
8
+ export declare function toSearchParams(query: JsonApiQuery): URLSearchParams;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toSearchParams = toSearchParams;
4
+ const node_url_1 = require("node:url");
5
+ /**
6
+ * Call this function from clients to convert from {@link JsonApiQuery} to {@link URLSearchParams}.
7
+ *
8
+ * @see [Example](https://github.com/ClipboardHealth/core-utils/blob/main/packages/json-api/examples/toSearchParams.ts)
9
+ */
10
+ function toSearchParams(query) {
11
+ const searchParams = new node_url_1.URLSearchParams();
12
+ if (query.fields) {
13
+ Object.entries(query.fields).forEach(([type, fields]) => {
14
+ searchParams.append(`fields[${type}]`, fields.join(","));
15
+ });
16
+ }
17
+ if (query.filter) {
18
+ Object.entries(query.filter).forEach(([field, values]) => {
19
+ if (Array.isArray(values)) {
20
+ searchParams.append(`filter[${field}]`, values.join(","));
21
+ }
22
+ else if (typeof values === "object") {
23
+ Object.entries(values).forEach(([fieldType, value]) => {
24
+ searchParams.append(`filter[${field}][${fieldType}]`, value);
25
+ });
26
+ }
27
+ });
28
+ }
29
+ if (query.include) {
30
+ searchParams.append("include", query.include.join(","));
31
+ }
32
+ if (query.page) {
33
+ Object.entries(query.page).forEach(([key, value]) => {
34
+ searchParams.append(`page[${key}]`, value);
35
+ });
36
+ }
37
+ if (query.sort) {
38
+ searchParams.append("sort", query.sort.join(","));
39
+ }
40
+ return searchParams;
41
+ }
42
+ //# sourceMappingURL=toSearchParams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toSearchParams.js","sourceRoot":"","sources":["../../../../../packages/json-api/src/lib/toSearchParams.ts"],"names":[],"mappings":";;AASA,wCAoCC;AA7CD,uCAA2C;AAI3C;;;;GAIG;AACH,SAAgB,cAAc,CAAC,KAAmB;IAChD,MAAM,YAAY,GAAG,IAAI,0BAAe,EAAE,CAAC;IAE3C,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE;YACtD,YAAY,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE;YACvD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1B,YAAY,CAAC,MAAM,CAAC,UAAU,KAAK,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5D,CAAC;iBAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACtC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE;oBACpD,YAAY,CAAC,MAAM,CAAC,UAAU,KAAK,KAAK,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC/D,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,YAAY,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAClD,YAAY,CAAC,MAAM,CAAC,QAAQ,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Filter operators to build complex filter queries.
3
+ * - gt: Greater than
4
+ * - gte: Greater than or equal to
5
+ * - lt: Less than
6
+ * - lte: Less than or equal to
7
+ * - not: Not equal to
8
+ */
9
+ type FieldType = "gt" | "gte" | "lt" | "lte" | "not";
10
+ /**
11
+ * A JSON:API URL query string.
12
+ */
13
+ export interface JsonApiQuery {
14
+ /**
15
+ * Fields to include in the response.
16
+ *
17
+ * @see {@link https://jsonapi.org/format/#fetching-sparse-fieldsets Sparse fieldsets}
18
+ */
19
+ fields?: Record<string, string[]>;
20
+ /**
21
+ * Filters to apply to the query.
22
+ *
23
+ * @see {@link https://jsonapi.org/recommendations/#filtering Filtering}
24
+ * @see {@link https://discuss.jsonapi.org/t/share-propose-a-filtering-strategy/257 Filtering strategy}
25
+ */
26
+ filter?: Record<string, string[] | {
27
+ [K in FieldType]?: string;
28
+ }>;
29
+ /**
30
+ * Relationships to include in the response.
31
+ *
32
+ * @see {@link https://jsonapi.org/format/#fetching-includes Fetching includes}
33
+ */
34
+ include?: string[];
35
+ /**
36
+ * Pagination data.
37
+ *
38
+ * @see {@link https://jsonapi.org/format/#fetching-pagination Pagination}
39
+ * @see {@link https://jsonapi.org/examples/#pagination Pagination examples}
40
+ */
41
+ page?: {
42
+ [K in "cursor" | "limit" | "number" | "offset" | "size"]?: string;
43
+ };
44
+ /**
45
+ * Sorting data. Include the "-" prefix for descending order.
46
+ *
47
+ * @see {@link https://jsonapi.org/format/#fetching-sorting Sorting}
48
+ */
49
+ sort?: string[];
50
+ }
51
+ export {};
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../packages/json-api/src/lib/types.ts"],"names":[],"mappings":""}