Skip to content

Commit

Permalink
feat: json-api query helpers (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
therockstorm authored Aug 23, 2024
1 parent 1bc2ea1 commit 48bf5fd
Show file tree
Hide file tree
Showing 23 changed files with 625 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
# GitHub checks PRs out based on the merge commit; we want the branch HEAD.
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- run: git fetch origin main:main
- uses: nrwl/nx-set-shas@v4
- uses: actions/cache@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/bin/sh

npx lint-staged

if command -v gitleaks >/dev/null; then
Expand Down
12 changes: 8 additions & 4 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"ignorePaths": [
"coverage",
"dist",
"node_modules",
".github/CODEOWNERS",
".vscode",
"coverage",
"default.json",
"tmp"
"dist",
"node_modules",
"tmp",
"packages/json-api/examples/toJsonApiQuery.ts",
"packages/json-api/README.md",
"packages/json-api/src/lib/toSearchParams.spec.ts"
],
"words": [
"amannn",
Expand All @@ -17,6 +20,7 @@
"CODEOWNERS",
"devkit",
"embedme",
"fieldsets",
"gitleaks",
"lcov",
"maxage",
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/json-api/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"parserOptions": {
"project": "tsconfig.lint.json",
"tsconfigRootDir": "packages/json-api"
}
}
70 changes: 70 additions & 0 deletions packages/json-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# @clipboard-health/json-api

Utilities for adhering to the [JSON:API](https://jsonapi.org/) specification.

## Table of Contents

- [Install](#install)
- [Usage](#usage)
- [Local development commands](#local-development-commands)

## Install

```bash
npm install @clipboard-health/json-api
```

## Usage

### Query helpers

From the client, call `toSearchParams` to convert from `JsonApiQuery` to `URLSearchParams`:

<!-- prettier-ignore -->
```ts
// ./examples/toSearchParams.ts

import { toSearchParams } from "@clipboard-health/json-api";

import { type JsonApiQuery } from "../src/lib/types";

const query: JsonApiQuery = {
fields: { dog: ["age", "name"] },
filter: { age: ["2", "5"] },
include: ["vet"],
page: { size: "10" },
sort: ["-age"],
};

console.log(toSearchParams(query).toString());
// Note: actual result is URL-encoded, but unencoded below for readability
// => fields[dog]=age,name&filter[age]=2,5&include=vet&page[size]=10&sort=-age

```

From the server, call `toJsonApiQuery` to convert from `URLSearchParams` to `JsonApiQuery`:

<!-- prettier-ignore -->
```ts
// ./examples/toJsonApiQuery.ts

import { toJsonApiQuery } from "@clipboard-health/json-api";

const searchParams = new URLSearchParams(
"fields%5Bdog%5D=age%2Cname&filter%5Bage%5D=2%2C5&include=vet&page%5Bsize%5D=10&sort=-age",
);

console.log(toJsonApiQuery(searchParams));
// => {
// fields: { dog: ["age", "name"] },
// filter: { age: ["2", "5"] },
// include: ["vet"],
// page: { size: "10" },
// sort: ["-age"],
// }

```

## Local development commands

See [`package.json`](./package.json) `scripts` for a list of commands.
14 changes: 14 additions & 0 deletions packages/json-api/examples/toJsonApiQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { toJsonApiQuery } from "@clipboard-health/json-api";

const searchParams = new URLSearchParams(
"fields%5Bdog%5D=age%2Cname&filter%5Bage%5D=2%2C5&include=vet&page%5Bsize%5D=10&sort=-age",
);

console.log(toJsonApiQuery(searchParams));
// => {
// fields: { dog: ["age", "name"] },
// filter: { age: ["2", "5"] },
// include: ["vet"],
// page: { size: "10" },
// sort: ["-age"],
// }
15 changes: 15 additions & 0 deletions packages/json-api/examples/toSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { toSearchParams } from "@clipboard-health/json-api";

import { type JsonApiQuery } from "../src/lib/types";

const query: JsonApiQuery = {
fields: { dog: ["age", "name"] },
filter: { age: ["2", "5"] },
include: ["vet"],
page: { size: "10" },
sort: ["-age"],
};

console.log(toSearchParams(query).toString());
// Note: actual result is URL-encoded, but unencoded below for readability
// => fields[dog]=age,name&filter[age]=2,5&include=vet&page[size]=10&sort=-age
19 changes: 19 additions & 0 deletions packages/json-api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default {
coverageDirectory: "../../coverage/packages/json-api",
coveragePathIgnorePatterns: [],
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
displayName: "json-api",
moduleFileExtensions: ["ts", "js"],
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
};
25 changes: 25 additions & 0 deletions packages/json-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@clipboard-health/json-api",
"description": "",
"version": "0.0.0",
"bugs": "https://github.com/clipboardhealth/core-utils/issues",
"dependencies": {
"tslib": "2.6.3"
},
"keywords": [],
"license": "MIT",
"main": "./src/index.js",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/clipboardhealth/core-utils.git",
"directory": "packages/json-api"
},
"scripts": {
"embed": "embedme README.md"
},
"type": "commonjs",
"typings": "./src/index.d.ts"
}
33 changes: 33 additions & 0 deletions packages/json-api/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "json-api",
"projectType": "library",
"sourceRoot": "packages/json-api/src",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"options": {
"assets": ["packages/json-api/*.md"],
"main": "packages/json-api/src/index.js",
"outputPath": "dist/packages/json-api",
"tsConfig": "packages/json-api/tsconfig.lib.json"
},
"outputs": ["{options.outputPath}"]
},
"lint": {
"executor": "@nx/eslint:lint",
"options": {
"lintFilePatterns": ["packages/json-api/**/*.[jt]s"],
"maxWarnings": 0
},
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "packages/json-api/jest.config.ts"
}
}
}
}
3 changes: 3 additions & 0 deletions packages/json-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./lib/toJsonApiQuery";
export * from "./lib/toSearchParams";
export * from "./lib/types";
99 changes: 99 additions & 0 deletions packages/json-api/src/lib/toJsonApiQuery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { toJsonApiQuery } from "./toJsonApiQuery";

const BASE_URL = "https://google.com";

describe("toJsonApiQuery", () => {
it("returns empty object if no matches", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?hi=there`).searchParams)).toEqual({});
});

it("parses fields", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?fields[dog]=age`).searchParams)).toEqual({
fields: { dog: ["age"] },
});
});

it("parses fields with multiple values", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?fields[dog]=age,name`).searchParams)).toEqual({
fields: { dog: ["age", "name"] },
});
});

it("parses filter", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?filter[name]=alice`).searchParams)).toEqual({
filter: { name: ["alice"] },
});
});

it("parses multiple filters", () => {
expect(
toJsonApiQuery(new URL(`${BASE_URL}?filter[name]=alice&filter[age]=2`).searchParams),
).toEqual({
filter: { age: ["2"], name: ["alice"] },
});
});

it("parses filters with multiple values", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?filter[name]=alice,bob`).searchParams)).toEqual({
filter: { name: ["alice", "bob"] },
});
});

it("parses filter type gte", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?filter[age][gte]=2`).searchParams)).toEqual({
filter: { age: { gte: "2" } },
});
});

it("parses include", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?include=owner`).searchParams)).toEqual({
include: ["owner"],
});
});

it("parses include with multiple values", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?include=owner,vet`).searchParams)).toEqual({
include: ["owner", "vet"],
});
});

it("parses page", () => {
expect(
toJsonApiQuery(new URL(`${BASE_URL}?page[size]=10&page[cursor]=a2c12`).searchParams),
).toEqual({
page: {
cursor: "a2c12",
size: "10",
},
});
});

it("parses sort", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?sort=age`).searchParams)).toEqual({
sort: ["age"],
});
});

it("parses sort with multiple values", () => {
expect(toJsonApiQuery(new URL(`${BASE_URL}?sort=age,-name`).searchParams)).toEqual({
sort: ["age", "-name"],
});
});

it("parses combinations", () => {
expect(
toJsonApiQuery(
new URL(`${BASE_URL}?filter[age]=2&include=vet&sort=age&fields[dog]=age&page[size]=10`)
.searchParams,
),
).toEqual({
fields: { dog: ["age"] },
filter: { age: ["2"] },
include: ["vet"],
page: {
size: "10",
},
sort: ["age"],
});
});
});
Loading

0 comments on commit 48bf5fd

Please sign in to comment.