├── .browserslistrc ├── packages ├── json-api-client │ ├── src │ │ ├── enums │ │ │ ├── index.ts │ │ │ └── http.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ └── json-api.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── json-api-body-builder.ts │ │ │ └── json-api-body-builder.test.ts │ │ ├── helpers │ │ │ ├── index.ts │ │ │ ├── is-deeper-than-one-nesting.ts │ │ │ └── type-checks.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── server-errors.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── index.ts │ │ │ ├── set-json-api-headers.test.ts │ │ │ ├── parse-json-api-response.ts │ │ │ ├── set-json-api-headers.ts │ │ │ ├── parse-json-api-error.ts │ │ │ ├── parse-json-api-error.test.ts │ │ │ ├── flatten-to-axios-json-api-query.ts │ │ │ └── flatten-to-axios-json-api-query.test.ts │ │ ├── test │ │ │ ├── index.ts │ │ │ ├── mock-wrapper.ts │ │ │ └── mocks │ │ │ │ ├── json-api-response-without-links-raw.json │ │ │ │ ├── json-api-response-raw.json │ │ │ │ └── json-api-response-parsed.json │ │ ├── browser.ts │ │ ├── response.ts │ │ ├── json-api.test.ts │ │ ├── response.test.ts │ │ └── json-api.ts │ ├── babel.config.json │ ├── babel.config.node.json │ ├── jest.config.js │ ├── tsconfig.json │ ├── esbuild.js │ ├── package.json │ └── README.md └── utils │ ├── babel.config.json │ ├── src │ ├── types │ │ ├── index.ts │ │ ├── event-emitter.ts │ │ └── time.ts │ ├── index.ts │ ├── browser.ts │ ├── event-emitter.ts │ ├── event-emitter.test.ts │ ├── duration.ts │ ├── bn.ts │ └── time.ts │ ├── babel.config.node.json │ ├── jest.config.js │ ├── tsconfig.json │ ├── esbuild.js │ ├── package.json │ └── README.md ├── jest.config.js ├── typedoc.json ├── lerna.json ├── README.md ├── .eslintignore ├── .prettierrc ├── babel.config.base.node.json ├── .editorconfig ├── .gitattributes ├── jest.config.base.js ├── .gitignore ├── .github └── workflows │ ├── lint-and-test.yaml │ └── deploy-gh-pages.yaml ├── .npmignore ├── tsconfig.base.json ├── babel.config.base.json ├── LICENSE ├── .eslintrc.js └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /packages/json-api-client/src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http' 2 | -------------------------------------------------------------------------------- /packages/json-api-client/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './json-api' 2 | -------------------------------------------------------------------------------- /packages/utils/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './time' 2 | export * from './event-emitter' 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ '/packages/*/jest.config.js'] 3 | } 4 | -------------------------------------------------------------------------------- /packages/json-api-client/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/babel.config.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.base.node.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/json-api-client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { JsonApiBodyBuilder } from './json-api-body-builder' 2 | -------------------------------------------------------------------------------- /packages/json-api-client/babel.config.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.base.node.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/json-api-client/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './type-checks' 2 | export * from './is-deeper-than-one-nesting' 3 | -------------------------------------------------------------------------------- /packages/utils/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../jest.config.base') 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | } 6 | -------------------------------------------------------------------------------- /packages/json-api-client/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../jest.config.base') 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | } 6 | -------------------------------------------------------------------------------- /packages/json-api-client/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | import * as serverErrors from './server-errors' 2 | export * from './server-errors' 3 | export const errors = { ...serverErrors } 4 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/bn' 2 | export * from '@/time' 3 | export * from '@/types' 4 | export * from '@/duration' 5 | export * from '@/event-emitter' 6 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "out": "doc", 4 | "entryPointStrategy": "packages", 5 | "entryPoints": ["./packages/**"] 6 | } 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "useNx": false, 6 | "version": "1.0.2", 7 | "npmClient": "yarn", 8 | "useWorkspaces": true 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-kit 2 | Front-end web kit for Distributed Lab projects 3 | 4 | ### This repository moved to the [new repository](https://github.com/distributed-lab/web-kit) and not be maintained anymore. 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | dist/*.js 3 | dist-browser/*.js 4 | config/*.js 5 | scripts/*.js 6 | playground 7 | *.schema.js 8 | index.html 9 | *.md 10 | *.config.js 11 | .eslintrc.js 12 | /dist/ 13 | esbuild.js 14 | -------------------------------------------------------------------------------- /packages/json-api-client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/json-api' 2 | export * from '@/response' 3 | export * from '@/errors' 4 | export * from '@/types' 5 | export * from '@/helpers' 6 | export * from '@/enums' 7 | export * from '@/utils' 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "bracketSameLine": false, 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto", 9 | "printWidth": 80 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.base.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./babel.config.base", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "modules": "cjs" 8 | } 9 | ], 10 | "@babel/preset-typescript" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/json-api-client/src/enums/http.ts: -------------------------------------------------------------------------------- 1 | export { StatusCodes as HTTP_STATUS_CODES } from 'http-status-codes' 2 | 3 | export enum HTTP_METHODS { 4 | GET = 'GET', 5 | POST = 'POST', 6 | PATCH = 'PATCH', 7 | PUT = 'PUT', 8 | DELETE = 'DELETE', 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = 0 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { flattenToAxiosJsonApiQuery } from './flatten-to-axios-json-api-query' 2 | export { parseJsonApiError } from './parse-json-api-error' 3 | export { parseJsonApiResponse } from './parse-json-api-response' 4 | export { setJsonApiHeaders } from './set-json-api-headers' 5 | -------------------------------------------------------------------------------- /packages/json-api-client/src/test/index.ts: -------------------------------------------------------------------------------- 1 | export { MockWrapper } from './mock-wrapper' 2 | export { default as RAW_RESPONSE } from './mocks/json-api-response-raw.json' 3 | export { default as WITHOUT_LINKS_RAW_RESPONSE } from './mocks/json-api-response-without-links-raw.json' 4 | export { default as PARSED_RESPONSE } from './mocks/json-api-response-parsed.json' 5 | -------------------------------------------------------------------------------- /packages/utils/src/browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is the entrypoint of browser builds. 3 | * The code executes when loaded in a browser. 4 | */ 5 | import * as utils from './index' 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | ;(window as any).webKitUtils = utils 9 | 10 | console.warn('Web Kit Utils was added to the window object.') 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case devs don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have LF line endings on checkout. 5 | *.* text eol=lf 6 | 7 | # Denote all files that are truly binary and should not be modified. 8 | *.png binary 9 | *.jpg binary 10 | *.otf binary 11 | *.ttf binary 12 | *.woff binary 13 | *.woff2 binary 14 | -------------------------------------------------------------------------------- /packages/json-api-client/src/browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is the entrypoint of browser builds. 3 | * The code executes when loaded in a browser. 4 | */ 5 | import * as JsonApi from './index' 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | ;(window as any).webKitJsonApi = JsonApi 9 | 10 | console.warn('Web Kit JSON API Client was added to the window object.') 11 | -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./jest.config.base') 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | roots: ['/src'], 6 | testMatch: [ 7 | '**/__tests__/**/*.+(ts|tsx|js)', 8 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 9 | ], 10 | transform: { 11 | '^.+\\.(ts|tsx)$': 'ts-jest', 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)': '/src/$1', 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/json-api-client/src/helpers/is-deeper-than-one-nesting.ts: -------------------------------------------------------------------------------- 1 | import { isObjectOrArray } from '@/helpers/type-checks' 2 | 3 | export function isDeeperThanOneNesting( 4 | object = {} as T, 5 | ): boolean { 6 | return Object.values(object) 7 | .filter(value => isObjectOrArray(value)) 8 | .reduce((acc: T[], cur: T) => acc.concat(Object.values(cur)), []) 9 | .some((value: T) => isObjectOrArray(value)) 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/src/types/event-emitter.ts: -------------------------------------------------------------------------------- 1 | export type EventMap = Record 2 | 3 | export type EventMapKey = string & keyof T 4 | 5 | export type EventHandler = (params: T) => void 6 | 7 | export type EventHandlers = Array< 8 | (p: T[K]) => void 9 | > 10 | 11 | export type EventHandlersMap = { 12 | [K in keyof T]?: EventHandlers 13 | } 14 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/set-json-api-headers.test.ts: -------------------------------------------------------------------------------- 1 | import { setJsonApiHeaders } from './set-json-api-headers' 2 | 3 | describe('setJsonapiHeaders', () => { 4 | it('should set proper set of headers', () => { 5 | const headers = setJsonApiHeaders({ headers: {} }) 6 | 7 | expect(headers).toStrictEqual({ 8 | 'Content-Type': 'application/vnd.api+json', 9 | Accept: 'application/vnd.api+json', 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/types", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["src/*"] 8 | }, 9 | "types": [ 10 | "node", 11 | "jest" 12 | ], 13 | "lib": [ 14 | "ES6", 15 | "DOM" 16 | ] 17 | }, 18 | "include": [ 19 | "src/**/*.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.test.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/json-api-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/types", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["src/*"] 8 | }, 9 | "types": [ 10 | "node", 11 | "jest" 12 | ], 13 | "lib": [ 14 | "ES6", 15 | "DOM" 16 | ] 17 | }, 18 | "include": [ 19 | "src/**/*.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.test.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | packages/**/node_modules/ 4 | packages/**/build/ 5 | packages/**/lib/ 6 | packages/**/dist/ 7 | packages/**/dist-browser/ 8 | /doc/ 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | test/unit/coverage 13 | test/e2e/reports 14 | selenium-debug.log 15 | .eslintcache 16 | package-lock.json 17 | coverage 18 | *.log 19 | 20 | deprecated 21 | 22 | # Editor directories and files 23 | .idea 24 | .vscode 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | -------------------------------------------------------------------------------- /packages/json-api-client/src/helpers/type-checks.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */ 2 | export const isObjectOrArray = (arg: unknown): boolean => { 3 | return arg instanceof Object 4 | } 5 | 6 | export const isUndefined = (arg: unknown): arg is undefined => { 7 | return typeof arg === 'undefined' 8 | } 9 | 10 | export const isObject = (arg: unknown): boolean => { 11 | return !Array.isArray(arg) && arg instanceof Object 12 | } 13 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/parse-json-api-response.ts: -------------------------------------------------------------------------------- 1 | import { JsonApiResponse } from '@/response' 2 | import { AxiosResponse } from 'axios' 3 | import { JsonApiClient } from '@/json-api' 4 | import { JsonApiDefaultMeta } from '@/types' 5 | 6 | export const parseJsonApiResponse = (opts: { 7 | raw: AxiosResponse 8 | apiClient: JsonApiClient 9 | isNeedRaw: boolean 10 | withCredentials: boolean 11 | }) => { 12 | return new JsonApiResponse(opts) 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_and_test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | nodejs: [16] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | # https://github.com/actions/setup-node 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.nodejs }} 20 | 21 | - run: yarn install 22 | - run: yarn lint && yarn test 23 | - run: yarn build 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /.vscode/ 3 | /node_modules/ 4 | packages/**/node_modules/ 5 | /tmp/ 6 | .idea/* 7 | /docs/ 8 | 9 | coverage 10 | *.log 11 | 12 | package-lock.json 13 | /*.tgz 14 | /tmp* 15 | /mnt/ 16 | /package/ 17 | /src/test/ 18 | *.test.ts 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | babel-debug.log 24 | 25 | .browserslistrc 26 | .editorconfig 27 | .eslintignore 28 | .eslintrc.js 29 | .gitlab-ci.yml 30 | jest.config.js 31 | .prettierrc 32 | tsconfig.json 33 | tsconfig.base.json 34 | lerna.json 35 | /scripts/ 36 | esbuild.js 37 | babel.config.json 38 | babel.config.node.json 39 | -------------------------------------------------------------------------------- /packages/utils/esbuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const esbuild = require('esbuild') 4 | const plugin = require('node-stdlib-browser/helpers/esbuild/plugin') 5 | const stdLibBrowser = require('node-stdlib-browser') 6 | 7 | ;(async () => { 8 | await esbuild.build({ 9 | entryPoints: ['src/browser.ts'], 10 | bundle: true, 11 | minify: true, 12 | sourcemap: 'external', 13 | outfile: 'lib/utils.min.js', 14 | inject: [require.resolve('node-stdlib-browser/helpers/esbuild/shim')], 15 | define: { 16 | global: 'global', 17 | process: 'process', 18 | Buffer: 'Buffer', 19 | }, 20 | plugins: [plugin(stdLibBrowser)], 21 | }) 22 | })() 23 | -------------------------------------------------------------------------------- /packages/json-api-client/esbuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const esbuild = require('esbuild') 4 | const plugin = require('node-stdlib-browser/helpers/esbuild/plugin') 5 | const stdLibBrowser = require('node-stdlib-browser') 6 | 7 | ;(async () => { 8 | await esbuild.build({ 9 | entryPoints: ['src/browser.ts'], 10 | bundle: true, 11 | minify: true, 12 | sourcemap: 'external', 13 | outfile: 'lib/json-api-client.min.js', 14 | inject: [require.resolve('node-stdlib-browser/helpers/esbuild/shim')], 15 | define: { 16 | global: 'global', 17 | process: 'process', 18 | Buffer: 'Buffer', 19 | }, 20 | plugins: [plugin(stdLibBrowser)], 21 | }) 22 | })() 23 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "importHelpers": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "experimentalDecorators": true, 20 | "resolveJsonModule": true, 21 | "sourceMap": true, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /babel.config.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": "inline", 3 | "minified": true, 4 | "comments": false, 5 | "presets": [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "modules": false 10 | } 11 | ], 12 | "@babel/preset-typescript" 13 | ], 14 | "plugins": [ 15 | [ 16 | "@babel/plugin-transform-runtime", 17 | { 18 | "regenerator": true 19 | } 20 | ], 21 | "@babel/plugin-proposal-class-properties", 22 | [ 23 | "babel-plugin-root-import", 24 | { 25 | "rootPathSuffix": "./src", 26 | "rootPathPrefix": "@/" 27 | } 28 | ] 29 | ], 30 | "ignore": [ 31 | "**/*.test.ts", 32 | "src/test", 33 | "src/browser.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/json-api-client/src/test/mock-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosResponse, AxiosResponseHeaders } from 'axios' 2 | import { JsonApiClient } from '../index' 3 | 4 | export class MockWrapper { 5 | static makeAxiosResponse( 6 | data: T, 7 | status = 200, 8 | config?: AxiosRequestConfig, 9 | ): AxiosResponse { 10 | return { 11 | data, 12 | status, 13 | statusText: 'ok', 14 | headers: {} as AxiosResponseHeaders, 15 | config: config || ({} as AxiosRequestConfig), 16 | } as AxiosResponse 17 | } 18 | 19 | static getMockedApi(): jest.Mocked { 20 | return new JsonApiClient({ 21 | baseUrl: 'http://localhost:8095/core', 22 | }) as never 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/set-json-api-headers.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep' 2 | 3 | import { 4 | JsonApiClientRequestConfig, 5 | JsonApiClientRequestConfigHeaders, 6 | } from '@/types' 7 | 8 | const MIME_TYPE_JSON_API = 'application/vnd.api+json' 9 | const HEADER_CONTENT_TYPE = 'Content-Type' 10 | const HEADER_ACCEPT = 'Accept' 11 | 12 | export const setJsonApiHeaders = ( 13 | requestConfig: JsonApiClientRequestConfig, 14 | ): JsonApiClientRequestConfigHeaders => { 15 | const config = cloneDeep(requestConfig) 16 | 17 | config.headers = config.headers ?? {} 18 | 19 | config.headers[HEADER_CONTENT_TYPE] = MIME_TYPE_JSON_API 20 | config.headers[HEADER_ACCEPT] = MIME_TYPE_JSON_API 21 | 22 | return config.headers 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy-gh-pages.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Builds the docs and deploys to GitHub pages 3 | # 4 | # https://github.com/actions/setup-node 5 | # Using https://github.com/marketplace/actions/deploy-to-github-pages 6 | name: Deploy to Github pages 7 | 8 | on: 9 | push: 10 | #tags: 11 | # - v* 12 | branches: 13 | - main 14 | 15 | jobs: 16 | deploy_pages: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: '16' 24 | cache: 'yarn' 25 | - run: yarn install 26 | - run: yarn docs 27 | 28 | - run: touch doc/.nojekyll 29 | - name: Deploy docs 🚀 30 | uses: JamesIves/github-pages-deploy-action@releases/v3 31 | with: 32 | BRANCH: gh-pages # The branch the action should deploy to. 33 | FOLDER: doc # The folder the action should deploy. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Distributed Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/utils/src/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { EventMapKey, EventMap, EventHandler, EventHandlersMap } from '@/types' 2 | 3 | export class EventEmitter { 4 | #handlers: EventHandlersMap = {} 5 | 6 | get handlers(): EventHandlersMap { 7 | return this.#handlers 8 | } 9 | 10 | public on>(key: K, fn: EventHandler): void { 11 | this.#handlers[key] = (this.#handlers[key] || [])?.concat(fn) 12 | } 13 | 14 | public once>(key: K, fn: EventHandler): void { 15 | const handler = (data: T[K]) => { 16 | fn(data) 17 | this.off(key, handler) 18 | } 19 | this.on(key, handler) 20 | } 21 | 22 | public off>(key: K, fn: EventHandler): void { 23 | this.#handlers[key] = (this.#handlers[key] || [])?.filter(f => f !== fn) 24 | } 25 | 26 | public emit>(key: K, data: T[K]): void | never { 27 | ;(this.#handlers[key] || [])?.forEach((fn: EventHandler) => { 28 | fn(data) 29 | }) 30 | } 31 | 32 | public clear(): void { 33 | this.#handlers = {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | jest: true, 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint'], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:prettier/recommended', 14 | ], 15 | ignorePatterns: ["lib", "node_modules", "examples", "scripts"], 16 | rules: { 17 | 'prettier/prettier': [ 18 | 'warn', 19 | { 20 | printWidth: 80, 21 | trailingComma: 'all', 22 | endOfLine: 'auto', 23 | }, 24 | ], 25 | 'arrow-parens': 0, 26 | 'no-debugger': 1, 27 | 'no-warning-comments': [ 28 | 1, 29 | { 30 | terms: ['hardcoded'], 31 | location: 'anywhere', 32 | }, 33 | ], 34 | 'no-console': [ 35 | 1, 36 | { 37 | allow: ['warn', 'error'], 38 | }, 39 | ], 40 | 'no-return-await': 0, 41 | 'object-curly-spacing': ['error', 'always'], 42 | 'no-var': 'error', 43 | 'comma-dangle': [1, 'always-multiline'], 44 | 'linebreak-style': ['error', 'unix'], 45 | '@typescript-eslint/no-var-requires': 0, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/parse-json-api-error.ts: -------------------------------------------------------------------------------- 1 | import { errors } from '@/errors' 2 | import { JsonApiError } from '@/errors' 3 | import { AxiosError } from 'axios' 4 | import { HTTP_STATUS_CODES } from '@/enums' 5 | import { JsonApiResponseErrors } from '@/types' 6 | 7 | /* 8 | * Parses server error and returns corresponding error instance. 9 | * Needed to handle on client side different behavior based on error type 10 | */ 11 | export const parseJsonApiError = ( 12 | error: AxiosError, 13 | ): JsonApiError => { 14 | const status = error?.response?.status 15 | 16 | switch (status) { 17 | case HTTP_STATUS_CODES.BAD_REQUEST: 18 | return new errors.BadRequestError(error) 19 | case HTTP_STATUS_CODES.UNAUTHORIZED: 20 | return new errors.UnauthorizedError(error) 21 | case HTTP_STATUS_CODES.FORBIDDEN: 22 | return new errors.ForbiddenError(error) 23 | case HTTP_STATUS_CODES.NOT_FOUND: 24 | return new errors.NotFoundError(error) 25 | case HTTP_STATUS_CODES.CONFLICT: 26 | return new errors.ConflictError(error) 27 | case HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR: 28 | return new errors.InternalServerError(error) 29 | default: 30 | return new errors.NetworkError(error) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@distributedlab/web-kit", 3 | "author": "Distributed Lab", 4 | "license": "MIT", 5 | "private": true, 6 | "keywords": [ 7 | "sdk", 8 | "distributed lab", 9 | "opensource" 10 | ], 11 | "description": "Front-end web kit for Distributed Lab projects", 12 | "workspaces": [ 13 | "packages/*" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/distributed-lab/web-kit.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/distributed-lab/web-kit/issues" 21 | }, 22 | "homepage": "https://github.com/distributed-lab/web-kit", 23 | "gitHooks": { 24 | "pre-commit": "yarn lint && lerna run type-check", 25 | "pre-push": "yarn test" 26 | }, 27 | "scripts": { 28 | "lint": "eslint packages/* --ext .ts,.tsx --fix", 29 | "build": "lerna run build", 30 | "docs": "typedoc --options typedoc.json", 31 | "test": "jest --verbose true", 32 | "test:watch": "jest --watch", 33 | "publish-packages": "yarn build && lerna publish from-package" 34 | }, 35 | "devDependencies": { 36 | "@typescript-eslint/eslint-plugin": "^5.30.7", 37 | "@typescript-eslint/parser": "^5.30.7", 38 | "eslint": "^8.20.0", 39 | "eslint-config-prettier": "^8.5.0", 40 | "eslint-plugin-prettier": "^4.2.1", 41 | "lerna": "^5.1.8", 42 | "prettier": "^2.7.1", 43 | "typedoc": "^0.23.8", 44 | "yorkie": "^2.0.0", 45 | "@types/jest": "^26.0.21", 46 | "jest": "^29.2.1", 47 | "ts-jest": "^29.0.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/json-api-client/src/utils/json-api-body-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonApiRecord, 3 | JsonApiRecordData, 4 | JsonApiAttributes, 5 | JsonApiRelationships, 6 | } from '@/types' 7 | 8 | export class JsonApiBodyBuilder { 9 | #body: JsonApiRecord 10 | 11 | constructor() { 12 | this.#body = { 13 | data: { 14 | type: '', 15 | }, 16 | } 17 | } 18 | 19 | public setData( 20 | data: JsonApiRecordData, 21 | ): JsonApiBodyBuilder { 22 | this.#body.data = data 23 | return this 24 | } 25 | 26 | public setIncluded( 27 | included: InstanceType[], 28 | ): JsonApiBodyBuilder { 29 | this.#body.included = included.map(record => record.build()) 30 | return this 31 | } 32 | 33 | public setType(type: T): JsonApiBodyBuilder { 34 | this.#body.data.type = type 35 | return this 36 | } 37 | 38 | public setID(id: string): JsonApiBodyBuilder { 39 | this.#body.data.id = id 40 | return this 41 | } 42 | 43 | public setAttributes(attributes: JsonApiAttributes): JsonApiBodyBuilder { 44 | this.#body.data.attributes = attributes 45 | return this 46 | } 47 | 48 | public setRelationships( 49 | relationships: JsonApiRelationships, 50 | ): JsonApiBodyBuilder { 51 | this.#body.data.relationships = relationships 52 | return this 53 | } 54 | 55 | public build(): JsonApiRecord { 56 | const result: JsonApiRecord = { ...this.#body } 57 | return result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/parse-json-api-error.test.ts: -------------------------------------------------------------------------------- 1 | import { errors } from '../errors' 2 | import { parseJsonApiError } from './parse-json-api-error' 3 | import { AxiosError } from 'axios' 4 | import { JsonApiResponseErrors } from '../types' 5 | 6 | describe('errors', () => { 7 | const testCases = [ 8 | { 9 | name: 'Bad Request', 10 | status: 400, 11 | data: { errors: [{}] }, 12 | expectedError: errors.BadRequestError, 13 | }, 14 | { 15 | name: 'Not Allowed', 16 | status: 401, 17 | data: { errors: [{}] }, 18 | expectedError: errors.UnauthorizedError, 19 | }, 20 | { 21 | name: 'Forbidden', 22 | status: 403, 23 | data: { errors: [{}] }, 24 | expectedError: errors.ForbiddenError, 25 | }, 26 | { 27 | name: 'Not Found', 28 | status: 404, 29 | data: { errors: [{}] }, 30 | expectedError: errors.NotFoundError, 31 | }, 32 | { 33 | name: 'Conflict', 34 | status: 409, 35 | data: { errors: [{}] }, 36 | expectedError: errors.ConflictError, 37 | }, 38 | { 39 | name: 'Internal Server Error', 40 | status: 500, 41 | data: { errors: [{}] }, 42 | expectedError: errors.InternalServerError, 43 | }, 44 | { 45 | name: 'Unexpected error', 46 | status: 488, 47 | data: { errors: [{}] }, 48 | expectedError: errors.NetworkError, 49 | }, 50 | ] 51 | 52 | testCases.forEach(testCase => { 53 | it(`should parse "${testCase.name}" error.`, () => { 54 | const error = parseJsonApiError({ 55 | response: { 56 | status: testCase.status, 57 | data: testCase.data, 58 | }, 59 | } as AxiosError) 60 | 61 | expect(error).toBeInstanceOf(testCase.expectedError) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/utils/src/event-emitter.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './event-emitter' 2 | 3 | describe('performs EventEmitter unit test', () => { 4 | describe('performs on', () => { 5 | test('should call registered handler', () => { 6 | const eventEmitter = new EventEmitter<{ foo: string }>() 7 | 8 | eventEmitter.on('foo', data => { 9 | expect(data).toBe('bar') 10 | }) 11 | 12 | eventEmitter.emit('foo', 'bar') 13 | }) 14 | 15 | test('should call multiple registered handlers', () => { 16 | const eventEmitter = new EventEmitter<{ foo: string }>() 17 | 18 | let i = 0 19 | 20 | eventEmitter.on('foo', data => { 21 | expect(data).toBe('bar') 22 | i++ 23 | }) 24 | 25 | eventEmitter.on('foo', data => { 26 | expect(data).toBe('bar') 27 | expect(i).toBe(1) 28 | }) 29 | 30 | eventEmitter.emit('foo', 'bar') 31 | }) 32 | }) 33 | 34 | describe('performs once, should call handler once', () => { 35 | test('should call handler once', () => { 36 | const eventEmitter = new EventEmitter<{ foo: string }>() 37 | 38 | const handler = (data: string) => { 39 | expect(data).toBe('bar') 40 | } 41 | 42 | eventEmitter.once('foo', handler) 43 | eventEmitter.emit('foo', 'bar') 44 | 45 | expect( 46 | eventEmitter?.handlers?.foo?.find(i => i === handler), 47 | ).toBeUndefined() 48 | }) 49 | }) 50 | 51 | describe('performs off, should unregister handler', () => { 52 | const eventEmitter = new EventEmitter<{ foo: string }>() 53 | 54 | const handler = (data: string) => { 55 | expect(data).toBe('bar') 56 | } 57 | 58 | eventEmitter.on('foo', handler) 59 | eventEmitter.emit('foo', 'bar') 60 | eventEmitter.off('foo', handler) 61 | 62 | expect( 63 | eventEmitter?.handlers?.foo?.find(i => i === handler), 64 | ).toBeUndefined() 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/flatten-to-axios-json-api-query.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep' 2 | 3 | import { isDeeperThanOneNesting, isObject, isObjectOrArray } from '../helpers' 4 | import { JsonApiClientRequestConfig, JsonApiClientRequestParams } from '@/types' 5 | 6 | /** 7 | * flattenToAxiosJsonApiQuery is needed to provide easier interface for complex query 8 | * params. 9 | */ 10 | export const flattenToAxiosJsonApiQuery = ( 11 | requestConfig: JsonApiClientRequestConfig, 12 | ): JsonApiClientRequestParams => { 13 | const config = cloneDeep(requestConfig) 14 | 15 | if (isDeeperThanOneNesting(config.params)) { 16 | throw new Error( 17 | 'Nested arrays or objects are not allowed for using in query params', 18 | ) 19 | } 20 | 21 | config.params = { 22 | ...flattenArraysOnly(config.params), 23 | ...flattenObjectsOnly(config.params), 24 | ...flattenPrimitivesOnly(config.params), 25 | } 26 | 27 | return config.params 28 | } 29 | 30 | function flattenArraysOnly(object: T) { 31 | return Object.entries(object) 32 | .filter(([, value]) => Array.isArray(value)) 33 | .map(([key, value]) => [key, value.join(',')]) 34 | .reduce((res, [key, val]) => ({ ...res, ...{ [key]: val } }), {}) 35 | } 36 | 37 | function flattenObjectsOnly(object: T) { 38 | return Object.entries(object) 39 | .filter(([, value]) => isObject(value)) 40 | .map(([prefix, nestedObj]) => 41 | Object.entries(nestedObj).map(([key, value]) => [ 42 | `${prefix}[${key}]`, 43 | value, 44 | ]), 45 | ) 46 | .reduce((acc, row) => acc.concat(row), []) 47 | .reduce((res, [key, val]) => ({ ...res, ...{ [key as string]: val } }), {}) 48 | } 49 | 50 | function flattenPrimitivesOnly(object: T) { 51 | return Object.entries(object) 52 | .filter(([, value]) => !isObjectOrArray(value)) 53 | .reduce((res, [key, val]) => ({ ...res, ...{ [key]: val } }), {}) 54 | } 55 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@distributedlab/utils", 3 | "description": "Typescript-based utils for Distributed Lab projects", 4 | "version": "3.2.2", 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "main": "./lib/cjs/index.js", 10 | "module": "./lib/esm/index.js", 11 | "browser": "./lib/utils.min.js", 12 | "unpkg": "./lib/utils.min.js", 13 | "types": "./lib/types/index.d.ts", 14 | "exports": { 15 | ".": { 16 | "import": "./lib/esm/index.js", 17 | "require": "./lib/cjs/index.js" 18 | } 19 | }, 20 | "typedoc": { 21 | "entryPoint": "./src/index.ts", 22 | "readmeFile": "./README.md", 23 | "displayName": "@distributedlab/utils" 24 | }, 25 | "sideEffects": false, 26 | "scripts": { 27 | "type-check": "tsc --noEmit", 28 | "build": "yarn clean && yarn build:types && yarn build:cjs && yarn build:esm && yarn build:browser", 29 | "build:cjs": "babel src --out-dir lib/cjs --extensions \".ts,.tsx\" --config-file ./babel.config.node.json", 30 | "build:esm": "babel src --out-dir lib/esm --extensions \".ts,.tsx\" --config-file ./babel.config.json", 31 | "build:types": "tsc --emitDeclarationOnly && tsc-alias -p tsconfig.json", 32 | "build:browser": "node ./esbuild.js", 33 | "clean": "rm -rf lib" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.18.9", 37 | "@babel/core": "^7.18.9", 38 | "@babel/plugin-proposal-class-properties": "^7.18.6", 39 | "@babel/plugin-transform-runtime": "^7.18.9", 40 | "@babel/preset-env": "^7.18.9", 41 | "@babel/preset-typescript": "^7.18.6", 42 | "@types/node": "^18.0.6", 43 | "babel-plugin-root-import": "^6.6.0", 44 | "esbuild": "^0.14.49", 45 | "jest": "^29.2.1", 46 | "node-stdlib-browser": "^1.2.0", 47 | "tsc-alias": "^1.7.0", 48 | "typescript": "^4.7.4" 49 | }, 50 | "dependencies": { 51 | "@babel/runtime": "^7.19.0", 52 | "bignumber.js": "^9.1.0", 53 | "dayjs": "^1.11.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/json-api-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@distributedlab/json-api-client", 3 | "description": "API Client for JSON API", 4 | "version": "2.2.1", 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "main": "./lib/cjs/index.js", 10 | "module": "./lib/esm/index.js", 11 | "browser": "./lib/json-api-client.min.js", 12 | "unpkg": "./lib/json-api-client.min.js", 13 | "types": "./lib/types/index.d.ts", 14 | "exports": { 15 | ".": { 16 | "import": "./lib/esm/index.js", 17 | "require": "./lib/cjs/index.js" 18 | } 19 | }, 20 | "typedoc": { 21 | "entryPoint": "./src/index.ts", 22 | "readmeFile": "./README.md", 23 | "displayName": "@distributedlab/json-api-client" 24 | }, 25 | "sideEffects": false, 26 | "scripts": { 27 | "lint": "eslint src/* --ext .js,.jsx,.ts,.tsx --fix", 28 | "test": "yarn jest", 29 | "type-check": "tsc --noEmit", 30 | "build": "yarn clean && yarn build:types && yarn build:cjs && yarn build:esm && yarn build:browser", 31 | "build:cjs": "babel src --out-dir lib/cjs --extensions \".ts,.tsx\" --config-file ./babel.config.node.json", 32 | "build:esm": "babel src --out-dir lib/esm --extensions \".ts,.tsx\" --config-file ./babel.config.json", 33 | "build:types": "tsc --emitDeclarationOnly && tsc-alias -p tsconfig.json", 34 | "build:browser": "node ./esbuild.js", 35 | "clean": "rm -rf lib" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.18.9", 39 | "@babel/core": "^7.18.9", 40 | "@babel/plugin-proposal-class-properties": "^7.18.6", 41 | "@babel/plugin-transform-runtime": "^7.18.9", 42 | "@babel/preset-env": "^7.18.9", 43 | "@babel/preset-typescript": "^7.18.6", 44 | "@types/lodash": "^4.14.184", 45 | "@types/node": "^18.0.6", 46 | "babel-plugin-root-import": "^6.6.0", 47 | "esbuild": "^0.14.49", 48 | "jest": "^29.2.1", 49 | "node-stdlib-browser": "^1.2.0", 50 | "tsc-alias": "^1.7.0", 51 | "typescript": "^4.7.4" 52 | }, 53 | "dependencies": { 54 | "@babel/runtime": "^7.19.0", 55 | "axios": "^1.0.0", 56 | "http-status-codes": "^2.2.0", 57 | "jsona": "^1.9.3", 58 | "lodash": "^4.17.21" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/json-api-client/src/middlewares/flatten-to-axios-json-api-query.test.ts: -------------------------------------------------------------------------------- 1 | import { flattenToAxiosJsonApiQuery } from './flatten-to-axios-json-api-query' 2 | import { JsonApiClientRequestConfig } from '../types' 3 | 4 | describe('flattenToAxiosJsonApiQuery', () => { 5 | it('should not modify object with primitive query parameters', () => { 6 | const query = { 7 | foo: 'bar', 8 | fizz: 'buzz', 9 | } 10 | 11 | const params = flattenToAxiosJsonApiQuery({ 12 | params: query, 13 | } as JsonApiClientRequestConfig) 14 | 15 | expect(params).toStrictEqual(query) 16 | }) 17 | 18 | it('should properly modify object with array params', () => { 19 | const query = { 20 | param: ['fizz', 'bar', 'buzz'], 21 | param2: ['abc', 123, 'qqq'], 22 | } 23 | 24 | const params = flattenToAxiosJsonApiQuery({ 25 | params: query, 26 | } as JsonApiClientRequestConfig) 27 | 28 | expect(params).toStrictEqual({ 29 | param: 'fizz,bar,buzz', 30 | param2: 'abc,123,qqq', 31 | }) 32 | }) 33 | 34 | it('should properly modify object with object params', () => { 35 | const query = { 36 | filter: { 37 | first_name: 'John', 38 | min_age: 25, 39 | }, 40 | page: { 41 | number: 2, 42 | limit: 15, 43 | }, 44 | } 45 | 46 | const params = flattenToAxiosJsonApiQuery({ 47 | params: query, 48 | } as JsonApiClientRequestConfig) 49 | 50 | expect(params).toStrictEqual({ 51 | 'filter[first_name]': 'John', 52 | 'filter[min_age]': 25, 53 | 'page[number]': 2, 54 | 'page[limit]': 15, 55 | }) 56 | }) 57 | 58 | it('should throw the proper error when query has nested array param', () => { 59 | const query = { 60 | param: [1, 2, [3, 4]], 61 | } 62 | 63 | expect(() => 64 | flattenToAxiosJsonApiQuery({ 65 | params: query, 66 | } as JsonApiClientRequestConfig), 67 | ).toThrow( 68 | 'Nested arrays or objects are not allowed for using in query params', 69 | ) 70 | }) 71 | 72 | it('should throw the proper error when query has nested object param', () => { 73 | const query = { 74 | param: { 75 | nestedParam: { 76 | key: 'value', 77 | }, 78 | }, 79 | } 80 | 81 | expect(() => 82 | flattenToAxiosJsonApiQuery({ 83 | params: query, 84 | } as JsonApiClientRequestConfig), 85 | ).toThrow( 86 | 'Nested arrays or objects are not allowed for using in query params', 87 | ) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/utils/src/types/time.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs' 2 | 3 | export type IsoDate = string // RFC3339Nano ISO Date String 4 | 5 | export type UnixDate = number // Unix time 6 | 7 | export type Inclusivity = '()' | '[)' | '(]' | '[]' // Inclusivity 8 | 9 | export type TimeDate = string | number | Date | Dayjs | null | undefined 10 | 11 | export type TimeFormat = 12 | | { 13 | locale?: string 14 | format?: string 15 | utc?: boolean 16 | } 17 | | string 18 | | string[] 19 | 20 | export type TimeUnitShort = 'd' | 'D' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms' 21 | 22 | export type TimeUnitLong = 23 | | 'millisecond' 24 | | 'second' 25 | | 'minute' 26 | | 'hour' 27 | | 'day' 28 | | 'month' 29 | | 'year' 30 | | 'date' 31 | 32 | export type TimeUnitLongPlural = 33 | | 'milliseconds' 34 | | 'seconds' 35 | | 'minutes' 36 | | 'hours' 37 | | 'days' 38 | | 'months' 39 | | 'years' 40 | | 'dates' 41 | 42 | export type TimeUnit = TimeUnitLong | TimeUnitLongPlural | TimeUnitShort 43 | 44 | export type TimeOpUnit = TimeUnit | 'week' | 'weeks' | 'w' 45 | 46 | export type TimeManipulate = Exclude 47 | 48 | export type TimeCalendar = Partial<{ 49 | sameDay: string 50 | lastDay: string 51 | nextDay: string 52 | lastWeek: string 53 | nextWeek: string 54 | sameElse: string 55 | }> 56 | 57 | export type TimeLocale = Partial<{ 58 | name: string 59 | weekdays: string[] 60 | weekdaysShort: string[] 61 | weekdaysMin: string[] 62 | weekStart: number 63 | yearStart: number 64 | months: string[] 65 | monthsShort: string[] 66 | ordinal: (n: number) => string 67 | formats: { 68 | LTS: string 69 | LT: string 70 | L: string 71 | LL: string 72 | LLL: string 73 | LLLL: string 74 | l: string 75 | ll: string 76 | lll: string 77 | llll: string 78 | } 79 | relativeTime: { 80 | future: string 81 | past: string 82 | s: string 83 | m: string 84 | mm: string 85 | h: string 86 | hh: string 87 | d: string 88 | dd: string 89 | M: string 90 | MM: string 91 | y: string 92 | yy: string 93 | } 94 | meridiem: (hour?: number, minute?: number, isLowercase?: boolean) => string 95 | calendar: TimeCalendar 96 | }> 97 | 98 | export type DurationUnitsObject = Partial<{ 99 | [unit in Exclude | 'weeks']: number 100 | }> 101 | 102 | export type DurationUnitType = Exclude 103 | -------------------------------------------------------------------------------- /packages/json-api-client/src/types/json-api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AxiosInstance, 3 | RawAxiosRequestHeaders, 4 | AxiosRequestConfig, 5 | } from 'axios' 6 | import { HTTP_METHODS } from '@/enums' 7 | 8 | export enum JsonApiLinkFields { 9 | first = 'first', 10 | last = 'last', 11 | next = 'next', 12 | prev = 'prev', 13 | self = 'self', 14 | } 15 | 16 | export type URL = string 17 | 18 | export type Endpoint = string // e.g. `/users` 19 | 20 | export type JsonApiClientConfig = { 21 | baseUrl?: URL 22 | axios?: AxiosInstance 23 | } 24 | 25 | export type JsonApiClientRequestConfigHeaders = RawAxiosRequestHeaders 26 | 27 | export type JsonApiClientRequestParams = unknown 28 | 29 | export type JsonApiClientRequestConfig = AxiosRequestConfig 30 | 31 | export type JsonApiErrorMetaType = Record | unknown[] | unknown 32 | 33 | export type JsonApiRelationship = Record< 34 | string, 35 | JsonApiRecordBase 36 | > & 37 | JsonApiLinks 38 | 39 | export type JsonApiRelationships = Record< 40 | string, 41 | JsonApiRelationship 42 | > 43 | 44 | // Can be used in client code to extend and cast own entity types 45 | export type JsonApiRecordBase = { 46 | id: string 47 | type: T 48 | } 49 | 50 | export type JsonApiResponseLinks = { 51 | first?: Endpoint 52 | last?: Endpoint 53 | next?: Endpoint 54 | prev?: Endpoint 55 | self?: Endpoint 56 | } 57 | 58 | export type JsonApiClientRequestOpts = { 59 | endpoint: Endpoint 60 | method: HTTP_METHODS 61 | headers?: JsonApiClientRequestConfigHeaders 62 | data?: unknown 63 | query?: unknown 64 | contentType?: string 65 | isEmptyBodyAllowed?: boolean 66 | isNeedRaw?: boolean 67 | withCredentials?: boolean 68 | } 69 | 70 | export type JsonApiResponseError = { 71 | id?: string | number 72 | code?: string 73 | title?: string 74 | detail?: string 75 | status?: string 76 | source?: { 77 | pointer?: string 78 | parameter?: string 79 | header?: string 80 | } 81 | meta?: JsonApiErrorMetaType 82 | links?: JsonApiResponseLinks 83 | } 84 | 85 | export type JsonApiResponseNestedErrors = JsonApiResponseError[] 86 | 87 | export type JsonApiResponseErrors = { 88 | errors?: JsonApiResponseNestedErrors 89 | } 90 | 91 | export type JsonApiDefaultMeta = Record 92 | 93 | export type JsonApiRecord = { 94 | data: JsonApiRecordData 95 | included?: JsonApiRecord[] 96 | } & JsonApiLinks 97 | 98 | export type JsonApiRecordData = Omit< 99 | JsonApiRecordBase, 100 | 'id' 101 | > & 102 | Partial, 'id'>> & 103 | JsonApiLinks & { 104 | attributes?: JsonApiAttributes 105 | relationships?: JsonApiRelationships 106 | } 107 | 108 | export type JsonApiLinks = { 109 | links?: { [key in JsonApiLinkFields]?: Endpoint } 110 | } 111 | 112 | export type JsonApiAttributes = Record 113 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # @distributedlab/utils 2 | Typescript-based utils for Distributed Lab projects 3 | 4 | ![version (scoped package)](https://badgen.net/npm/v/@distributedlab/utils) 5 | ![types](https://badgen.net/npm/types/@distributedlab/utils) 6 | ![tree-shaking](https://badgen.net/bundlephobia/tree-shaking/@distributedlab/utils) 7 | ![checks](https://badgen.net/github/checks/distributed-lab/web-kit-old/main) 8 | 9 | ### This package moved to the [new repository](https://github.com/distributed-lab/web-kit) and not be maintained anymore. 10 | 11 | ## Changelog 12 | All notable changes to this project will be documented in this file. 13 | 14 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 15 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 16 | 17 |
3.2.2 18 |

Change

19 |
    20 |
  • Deprecate package
  • 21 |
22 |
23 |
3.2.1 24 |

Added

25 |
    26 |
  • clear method to EventEmitter
  • **** 27 |
28 |
29 |
3.2.0 30 |

Added

31 |
    32 |
  • EventEmitter
  • 33 |
34 |
35 |
3.1.0 36 |

Fixed

37 |
    38 |
  • duration() type
  • 39 |
40 |
41 |
3.0.0 42 |

Added

43 |
    44 |
  • Duration class
  • 45 |
46 |

Changed

47 |
    48 |
  • Extended Time class with new functionality
  • 49 |
50 |
51 |
3.0.0-rc.0 52 |

Change

53 |
    54 |
  • Refactored DateUtil to Time
  • 55 |
56 |
57 |
2.1.0-rc.0 58 |

Added

59 |
    60 |
  • Relative time in DateUtil
  • 61 |
62 |
63 |
2.0.0 64 |

Changed

65 |
    66 |
  • MathUtil refactored to BN
  • 67 |
68 |
69 |
1.1.0 70 |

Added

71 |
    72 |
  • Diff method to DateUtil
  • 73 |
74 |
75 |
1.0.4 76 |

Fixed

77 |
    78 |
  • Build content in NPM package
  • 79 |
80 |
81 |
1.0.3 82 |

Fixed

83 |
    84 |
  • types directory location
  • 85 |
86 |
87 |
1.0.2 88 |

Fixed

89 |
    90 |
  • @babel/runtime dependency
  • 91 |
92 |
93 |
1.0.1 94 |

Fixed

95 |
    96 |
  • Readme
  • 97 |
98 |
99 |
1.0.0 100 |

Under the hood changes

101 |
    102 |
  • Initiated project
  • 103 |
104 |
105 | 106 | -------------------------------------------------------------------------------- /packages/utils/src/duration.ts: -------------------------------------------------------------------------------- 1 | import { Duration as _Duration } from 'dayjs/plugin/duration' 2 | import { DurationUnitsObject, DurationUnitType } from '@/types' 3 | import dayjs from 'dayjs' 4 | 5 | export class Duration { 6 | readonly #duration: _Duration 7 | 8 | constructor( 9 | input: DurationUnitsObject | number | string | undefined, 10 | unit?: DurationUnitType | undefined, 11 | ) { 12 | this.#duration = this._duration(input, unit) 13 | } 14 | 15 | private _duration( 16 | input?: DurationUnitsObject | number | string, 17 | unit?: DurationUnitType, 18 | ): _Duration { 19 | if (typeof input == 'number') return dayjs.duration(input as number, unit) 20 | 21 | if (typeof input == 'string') return dayjs.duration(input as string) 22 | 23 | return dayjs.duration(input as DurationUnitsObject) 24 | } 25 | 26 | public get duration(): _Duration { 27 | return this.#duration 28 | } 29 | 30 | public get asMilliseconds(): number { 31 | return this.#duration.asMilliseconds() 32 | } 33 | 34 | public get milliseconds(): number { 35 | return this.#duration.milliseconds() 36 | } 37 | 38 | public get asSeconds(): number { 39 | return this.#duration.asSeconds() 40 | } 41 | 42 | public get seconds(): number { 43 | return this.#duration.seconds() 44 | } 45 | 46 | public get asMinutes(): number { 47 | return this.#duration.asMinutes() 48 | } 49 | 50 | public get minutes(): number { 51 | return this.#duration.minutes() 52 | } 53 | 54 | public get asHours(): number { 55 | return this.#duration.asHours() 56 | } 57 | 58 | public get hours(): number { 59 | return this.#duration.hours() 60 | } 61 | 62 | public get asDays(): number { 63 | return this.#duration.asDays() 64 | } 65 | 66 | public get days(): number { 67 | return this.#duration.days() 68 | } 69 | 70 | public get asWeeks(): number { 71 | return this.#duration.asWeeks() 72 | } 73 | 74 | public get weeks(): number { 75 | return this.#duration.weeks() 76 | } 77 | 78 | public get asMonths(): number { 79 | return this.#duration.asMonths() 80 | } 81 | 82 | public get months(): number { 83 | return this.#duration.months() 84 | } 85 | 86 | public get asYears(): number { 87 | return this.#duration.asYears() 88 | } 89 | 90 | public get years(): number { 91 | return this.#duration.years() 92 | } 93 | 94 | public get humanized(): string { 95 | return this.#duration.humanize() 96 | } 97 | 98 | public format(formatStr?: string): string { 99 | return this.#duration.format(formatStr) 100 | } 101 | } 102 | 103 | export function duration(units: DurationUnitsObject): Duration 104 | export function duration(time: number, unit?: DurationUnitType): Duration 105 | export function duration(ISO_8601: string): Duration 106 | export function duration( 107 | input?: DurationUnitsObject | number | string, 108 | unit?: DurationUnitType, 109 | ): Duration { 110 | return new Duration(input, unit) 111 | } 112 | -------------------------------------------------------------------------------- /packages/json-api-client/src/errors/server-errors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios' 2 | import { 3 | JsonApiErrorMetaType, 4 | JsonApiResponseErrors, 5 | JsonApiResponseNestedErrors, 6 | } from '@/types' 7 | 8 | /** 9 | * Base class for server errors. 10 | */ 11 | export class JsonApiErrorBase extends Error { 12 | originalError: AxiosError 13 | _meta: JsonApiErrorMetaType 14 | _detail: string 15 | _title: string | undefined 16 | _nestedErrors: JsonApiResponseNestedErrors 17 | 18 | constructor(originalError: AxiosError) { 19 | super() 20 | 21 | this.originalError = originalError 22 | this._meta = {} 23 | this._nestedErrors = [] 24 | this._title = '' 25 | this._detail = '' 26 | } 27 | 28 | /** 29 | * Response HTTP status. 30 | */ 31 | get httpStatus(): number | undefined { 32 | return this.originalError?.response?.status 33 | } 34 | 35 | /** 36 | * Error meta. 37 | */ 38 | get meta(): JsonApiErrorMetaType { 39 | return this._meta 40 | } 41 | 42 | /** 43 | * A short, human-readable summary of the problem. 44 | */ 45 | get title(): string | undefined { 46 | return this._title 47 | } 48 | 49 | /** 50 | * A human-readable explanation specific to this occurrence of the problem. 51 | */ 52 | get detail(): string { 53 | return this._detail 54 | } 55 | 56 | get requestPath(): string | undefined { 57 | return this.originalError?.response?.request?.path 58 | } 59 | } 60 | 61 | /** 62 | * Generic server error response. 63 | */ 64 | export class JsonApiError extends JsonApiErrorBase { 65 | constructor(originalError: AxiosError) { 66 | super(originalError) 67 | 68 | const unwrappedError = originalError?.response?.data?.errors?.[0] 69 | this.name = unwrappedError?.title ?? '' 70 | this._title = unwrappedError?.title ?? '' 71 | this._detail = unwrappedError?.detail ?? '' 72 | this._meta = unwrappedError?.meta || {} 73 | } 74 | } 75 | 76 | export class BadRequestError extends JsonApiError { 77 | /** 78 | * Wrap a raw API error response. 79 | */ 80 | constructor(originalError: AxiosError) { 81 | super(originalError) 82 | const errors = originalError?.response?.data?.errors || [] 83 | 84 | if (errors.length > 1) { 85 | this._title = 'Request contains some errors.' 86 | this._detail = 'Request contains some errors. Check "nestedErrors"' 87 | this._nestedErrors = errors.map(err => ({ 88 | title: err?.title, 89 | detail: err?.detail, 90 | meta: err?.meta, 91 | })) 92 | } 93 | } 94 | 95 | /** 96 | * Errors for every invalid field. 97 | */ 98 | get nestedErrors(): JsonApiResponseNestedErrors { 99 | return this._nestedErrors 100 | } 101 | } 102 | 103 | /** 104 | * 401(Unauthorized) error. 105 | */ 106 | export class UnauthorizedError extends JsonApiError {} 107 | 108 | /** 109 | * 403(Forbidden) error. 110 | */ 111 | export class ForbiddenError extends JsonApiError {} 112 | 113 | /** 114 | * (404)The requested resource was not found. 115 | */ 116 | export class NotFoundError extends JsonApiError {} 117 | 118 | /** 119 | * The request could not be completed due to a conflict with the current state 120 | * of the target resource. 121 | */ 122 | export class ConflictError extends JsonApiError {} 123 | 124 | /** 125 | * 500(Internal server) error 126 | */ 127 | export class InternalServerError extends JsonApiError {} 128 | 129 | /** 130 | * Network error. 131 | */ 132 | export class NetworkError extends JsonApiError {} 133 | -------------------------------------------------------------------------------- /packages/json-api-client/src/response.ts: -------------------------------------------------------------------------------- 1 | import Jsona from 'jsona' 2 | import isEmpty from 'lodash/isEmpty' 3 | 4 | import { AxiosResponse, RawAxiosResponseHeaders } from 'axios' 5 | import { 6 | Endpoint, 7 | JsonApiDefaultMeta, 8 | JsonApiLinkFields, 9 | JsonApiResponseLinks, 10 | } from './types' 11 | import { JsonApiClient } from '@/json-api' 12 | import { StatusCodes } from 'http-status-codes' 13 | import { HTTP_METHODS } from '@/enums' 14 | 15 | const formatter = new Jsona() 16 | 17 | /** 18 | * API response wrapper. 19 | */ 20 | export class JsonApiResponse { 21 | private _raw: AxiosResponse 22 | private _rawData!: Record 23 | private _data!: T 24 | private _links: JsonApiResponseLinks 25 | private _apiClient: JsonApiClient 26 | private _isNeedRaw: boolean 27 | private _withCredentials: boolean 28 | private _meta: U 29 | 30 | constructor(opts: { 31 | raw: AxiosResponse 32 | isNeedRaw: boolean 33 | apiClient: JsonApiClient 34 | withCredentials: boolean 35 | }) { 36 | this._raw = opts.raw 37 | this._rawData = opts.raw?.data 38 | this._links = opts.raw?.data?.links ?? {} 39 | this._apiClient = opts.apiClient 40 | this._isNeedRaw = opts.isNeedRaw 41 | this._withCredentials = opts.withCredentials 42 | this._parseResponse(opts.raw, opts.isNeedRaw) 43 | this._meta = opts.raw?.data?.meta || {} 44 | } 45 | 46 | get meta(): U { 47 | return this._meta 48 | } 49 | 50 | /** 51 | * Get raw response. 52 | */ 53 | get rawResponse(): AxiosResponse { 54 | return this._raw 55 | } 56 | 57 | /** 58 | * Get request page limit. 59 | */ 60 | get pageLimit(): number | undefined { 61 | const requestConfig = this._raw.config 62 | const pageLimitKey = 'page[limit]' 63 | 64 | if (!isEmpty(requestConfig.params)) { 65 | return requestConfig.params[pageLimitKey] 66 | } 67 | 68 | const decodedUrl = decodeURIComponent(requestConfig.url || '') 69 | const limit = new URLSearchParams(decodedUrl).get(pageLimitKey) 70 | 71 | return Number(limit) 72 | } 73 | 74 | /** 75 | * Get raw response data. 76 | */ 77 | get rawData(): Record { 78 | return this._rawData || {} 79 | } 80 | 81 | /** 82 | * Get response data. 83 | */ 84 | get data(): T { 85 | return this._data 86 | } 87 | 88 | /** 89 | * Get response HTTP status. 90 | */ 91 | get status(): number { 92 | return this._raw.status 93 | } 94 | 95 | /** 96 | * Get response headers. 97 | */ 98 | get headers(): RawAxiosResponseHeaders { 99 | return this._raw.headers 100 | } 101 | 102 | /** 103 | * Get response links. 104 | */ 105 | get links(): JsonApiResponseLinks { 106 | return this._links 107 | } 108 | 109 | /** 110 | * Is response links exist. 111 | */ 112 | get isLinksExist(): boolean { 113 | return Boolean(this._links) && !isEmpty(this._links) 114 | } 115 | 116 | /** 117 | * Parses and unwraps response data. 118 | */ 119 | private _parseResponse(raw: AxiosResponse, isNeedRaw: boolean) { 120 | if ( 121 | raw.status === StatusCodes.NO_CONTENT || 122 | raw.status === StatusCodes.RESET_CONTENT 123 | ) { 124 | return 125 | } 126 | 127 | this._data = isNeedRaw 128 | ? (raw.data as T) 129 | : (formatter.deserialize(raw.data) as T) 130 | } 131 | 132 | private _createLink(link: Endpoint): Endpoint { 133 | const baseUrl = this._apiClient?.baseUrl 134 | 135 | if (!baseUrl) return link 136 | 137 | let intersection = '' 138 | 139 | for (const char of link) { 140 | if (baseUrl.endsWith(intersection + char)) { 141 | intersection += char 142 | break 143 | } else { 144 | intersection += char 145 | } 146 | } 147 | 148 | return link.replace(intersection, '') 149 | } 150 | 151 | public async fetchPage( 152 | page: JsonApiLinkFields, 153 | ): Promise> { 154 | if (!this.isLinksExist) { 155 | throw new TypeError('There are no links in response') 156 | } 157 | 158 | const link = this._createLink(this.links[page] as string) 159 | 160 | const JsonApiClientRequestOpts = { 161 | endpoint: link, 162 | method: this._raw.config.method?.toUpperCase() as HTTP_METHODS, 163 | headers: this._raw.config.headers, 164 | isNeedRaw: this._isNeedRaw, 165 | withCredentials: this._withCredentials, 166 | } 167 | 168 | return this._apiClient.request(JsonApiClientRequestOpts) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /packages/json-api-client/src/json-api.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { RAW_RESPONSE, PARSED_RESPONSE } from './test' 3 | import { JsonApiClient } from './json-api' 4 | import { MockWrapper } from './test' 5 | 6 | jest.mock('axios') 7 | 8 | const mockedAxios = axios as jest.MockedFunction 9 | 10 | const mockedData = { 11 | foo: { 12 | bar: 'string', 13 | }, 14 | } 15 | 16 | beforeEach(() => { 17 | jest.resetModules() 18 | jest.clearAllMocks() 19 | }) 20 | 21 | describe('performs JsonApiClient request unit test', () => { 22 | describe('performs constructor', () => { 23 | test('should set base url if provided', () => { 24 | const api = new JsonApiClient({ baseUrl: 'foo' }) 25 | expect(api.baseUrl).toBe('foo') 26 | }) 27 | 28 | test('should set base url and auth token if provided', () => { 29 | const api = new JsonApiClient({ baseUrl: 'foo' }) 30 | expect(api.baseUrl).toBe('foo') 31 | }) 32 | 33 | test('base url and auth token should be empty if not provided', () => { 34 | const api = new JsonApiClient() 35 | expect(api.baseUrl).toBe('') 36 | }) 37 | }) 38 | 39 | describe('performs helper methods', () => { 40 | test('should throw exception "baseUrl" argument not provided', () => 41 | expect(() => new JsonApiClient().useBaseUrl('')).toThrow( 42 | 'Arg "baseUrl" not passed', 43 | )) 44 | 45 | test('should change base url', () => { 46 | const api = new JsonApiClient({ baseUrl: 'foo' }) 47 | 48 | expect(api.baseUrl).toBe('foo') 49 | 50 | api.useBaseUrl('bar') 51 | 52 | expect(api.baseUrl).toBe('bar') 53 | }) 54 | 55 | test('should throw exception if "baseUrl" argument not provided', () => 56 | expect(() => new JsonApiClient().withBaseUrl('')).toThrow( 57 | 'Arg "baseUrl" not passed', 58 | )) 59 | 60 | test('should return new client with new base url', () => { 61 | const api = new JsonApiClient({ baseUrl: 'foo' }) 62 | const apiWithNewBaseUrl = api.withBaseUrl('bar') 63 | 64 | expect(api.baseUrl).toBe('foo') 65 | expect(apiWithNewBaseUrl).toBeInstanceOf(JsonApiClient) 66 | expect(apiWithNewBaseUrl.baseUrl).toBe('bar') 67 | }) 68 | }) 69 | 70 | describe('performs "request()"', () => { 71 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 72 | 73 | let api: JsonApiClient 74 | 75 | beforeEach(() => { 76 | mockedAxios.mockResolvedValue(rawResponse) 77 | 78 | api = new JsonApiClient({ 79 | baseUrl: 'http://localhost:8095', 80 | axios: mockedAxios, 81 | }) 82 | 83 | jest.spyOn(api, 'request') 84 | }) 85 | 86 | test('"get()" should call "request()" with correct params', async () => { 87 | const query = { 88 | page: { 89 | limit: 100, 90 | }, 91 | } 92 | 93 | await api.get('/foo', query) 94 | 95 | expect(api.request).toHaveBeenLastCalledWith({ 96 | method: 'GET', 97 | endpoint: '/foo', 98 | query, 99 | isEmptyBodyAllowed: true, 100 | }) 101 | }) 102 | 103 | test('"post()" should call "request()" with correct params', async () => { 104 | await api.post('/foo', mockedData) 105 | 106 | expect(api.request).toHaveBeenLastCalledWith({ 107 | method: 'POST', 108 | endpoint: '/foo', 109 | data: mockedData, 110 | }) 111 | }) 112 | 113 | test('"patch()" should call "request()" with correct params', async () => { 114 | await api.patch('/foo', mockedData) 115 | 116 | expect(api.request).toHaveBeenLastCalledWith({ 117 | method: 'PATCH', 118 | endpoint: '/foo', 119 | data: mockedData, 120 | }) 121 | }) 122 | 123 | test('"put()" should call "request()" with correct params', async () => { 124 | await api.put('/foo', mockedData) 125 | 126 | expect(api.request).toHaveBeenLastCalledWith({ 127 | method: 'PUT', 128 | endpoint: '/foo', 129 | data: mockedData, 130 | }) 131 | }) 132 | 133 | test('"delete()" should call "request()" with correct params', async () => { 134 | await api.delete('/foo') 135 | 136 | expect(api.request).toHaveBeenLastCalledWith({ 137 | method: 'DELETE', 138 | endpoint: '/foo', 139 | data: undefined, 140 | isEmptyBodyAllowed: true, 141 | }) 142 | }) 143 | }) 144 | 145 | test('should return correct data', () => { 146 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 147 | mockedAxios.mockResolvedValueOnce(rawResponse) 148 | 149 | const api = new JsonApiClient({ 150 | baseUrl: 'http://localhost:8095', 151 | axios: mockedAxios, 152 | }) 153 | 154 | return api.get('').then(({ data }) => expect(data).toEqual(PARSED_RESPONSE)) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /packages/json-api-client/src/utils/json-api-body-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonApiBodyBuilder } from './json-api-body-builder' 2 | import { JsonApiRecord } from '../types' 3 | 4 | describe('performs JsonApiBodyBuilder unit test', () => { 5 | describe('setData', () => { 6 | it('should set data to data property of body', () => { 7 | const body: JsonApiRecord = new JsonApiBodyBuilder() 8 | .setData({ 9 | id: '120', 10 | type: 'book', 11 | attributes: { 12 | title: 'Blockchain Technology', 13 | part: '2', 14 | }, 15 | relationships: { 16 | autor: { 17 | data: { 18 | id: '1', 19 | type: 'company', 20 | }, 21 | }, 22 | }, 23 | links: { 24 | self: 'http://example.com/books/120', 25 | next: 'http://example.com/books', 26 | prev: 'http://example.com/books', 27 | last: 'http://example.com/books', 28 | first: 'http://example.com/books', 29 | }, 30 | }) 31 | .build() 32 | 33 | expect(body).toStrictEqual({ 34 | data: { 35 | id: '120', 36 | type: 'book', 37 | attributes: { 38 | title: 'Blockchain Technology', 39 | part: '2', 40 | }, 41 | relationships: { 42 | autor: { 43 | data: { 44 | id: '1', 45 | type: 'company', 46 | }, 47 | }, 48 | }, 49 | links: { 50 | self: 'http://example.com/books/120', 51 | next: 'http://example.com/books', 52 | prev: 'http://example.com/books', 53 | last: 'http://example.com/books', 54 | first: 'http://example.com/books', 55 | }, 56 | }, 57 | }) 58 | }) 59 | }) 60 | 61 | describe('setIncluded', () => { 62 | it('should set includedBook as array item to included property of body', () => { 63 | const includedBook: InstanceType = 64 | new JsonApiBodyBuilder() 65 | .setID('9') 66 | .setType('book') 67 | .setAttributes({ title: 'Blockchain Technology', part: '3' }) 68 | 69 | const body: JsonApiRecord = new JsonApiBodyBuilder() 70 | .setIncluded([includedBook]) 71 | .build() 72 | 73 | expect(body).toStrictEqual({ 74 | data: { 75 | type: '', 76 | }, 77 | included: [ 78 | { 79 | data: { 80 | id: '9', 81 | type: 'book', 82 | attributes: { 83 | title: 'Blockchain Technology', 84 | part: '3', 85 | }, 86 | }, 87 | }, 88 | ], 89 | }) 90 | }) 91 | }) 92 | 93 | describe('setType', () => { 94 | it('should set type to type property of data in body', () => { 95 | const body: JsonApiRecord = new JsonApiBodyBuilder() 96 | .setType('book') 97 | .build() 98 | 99 | expect(body).toStrictEqual({ 100 | data: { 101 | type: 'book', 102 | }, 103 | }) 104 | }) 105 | }) 106 | 107 | describe('setID', () => { 108 | it('should set id to id propery of data in body', () => { 109 | const body: JsonApiRecord = new JsonApiBodyBuilder().setID('28').build() 110 | 111 | expect(body).toStrictEqual({ 112 | data: { 113 | id: '28', 114 | type: '', 115 | }, 116 | }) 117 | }) 118 | }) 119 | 120 | describe('setAttributes', () => { 121 | it('should set attributes to attributes property of data in body', () => { 122 | const body: JsonApiRecord = new JsonApiBodyBuilder() 123 | .setAttributes({ 124 | title: 'Blockchain Technology', 125 | part: '2', 126 | }) 127 | .build() 128 | 129 | expect(body).toStrictEqual({ 130 | data: { 131 | type: '', 132 | attributes: { 133 | title: 'Blockchain Technology', 134 | part: '2', 135 | }, 136 | }, 137 | }) 138 | }) 139 | }) 140 | 141 | describe('setRelationships', () => { 142 | it('should set relationships to relationships property of data in body', () => { 143 | const body: JsonApiRecord = new JsonApiBodyBuilder() 144 | .setRelationships({ 145 | autor: { 146 | data: { 147 | id: '1', 148 | type: 'company', 149 | }, 150 | }, 151 | }) 152 | .build() 153 | 154 | expect(body).toStrictEqual({ 155 | data: { 156 | type: '', 157 | relationships: { 158 | autor: { 159 | data: { 160 | id: '1', 161 | type: 'company', 162 | }, 163 | }, 164 | }, 165 | }, 166 | }) 167 | }) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /packages/utils/src/bn.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | 3 | const WEI_DECIMALS = 18 4 | 5 | export enum BN_ROUNDING { 6 | DEFAULT = 4, 7 | UP = 0, 8 | DOWN = 1, 9 | CEIL = 2, 10 | FLOOR = 3, 11 | HALF_UP = 4, 12 | HALF_DOWN = 5, 13 | HALF_EVEN = 6, 14 | HALF_CEIL = 7, 15 | HALF_FLOOR = 8, 16 | } 17 | 18 | export interface BnCfg { 19 | decimals?: number 20 | rounding?: BN_ROUNDING 21 | noGroupSeparator?: boolean 22 | } 23 | 24 | export type BnFormatCfg = BigNumber.Format & BnCfg 25 | export type BnLike = string | number | BigNumber | BN 26 | 27 | BigNumber.config({ 28 | DECIMAL_PLACES: 0, 29 | ROUNDING_MODE: BN_ROUNDING.DEFAULT, 30 | FORMAT: { 31 | decimalSeparator: '.', 32 | groupSeparator: ',', 33 | groupSize: 3, 34 | }, 35 | }) 36 | 37 | export class BN { 38 | static TO_WEI_FACTOR = new BN(10).pow(WEI_DECIMALS) 39 | static MIN_WEI_STR = new BN(0.1).pow(WEI_DECIMALS).toString() 40 | static FROM_WEI_FACTOR = new BN(0.1).pow(WEI_DECIMALS) 41 | static ROUNDING = BN_ROUNDING 42 | static MAX_UINT256 = new BN(2).pow(256).sub(1) 43 | #bn: BigNumber 44 | #cfg?: BnCfg 45 | 46 | constructor(bigLike: BnLike, cfg?: BnCfg) { 47 | this.#bn = this._bn(bigLike, cfg) 48 | this.#cfg = cfg 49 | } 50 | 51 | static isBn(arg: unknown): arg is BN { 52 | return arg instanceof BN 53 | } 54 | 55 | static min(...args: BnLike[]): BN { 56 | return new BN(BigNumber.minimum(...args.map(e => new BN(e).#bn))) 57 | } 58 | 59 | static max(...args: BnLike[]): BN { 60 | return new BN(BigNumber.maximum(...args.map(e => new BN(e).#bn))) 61 | } 62 | 63 | mul(other: BnLike, cfg = this.#cfg): BN { 64 | return new BN(this.#bn.multipliedBy(this._bn(other)), cfg) 65 | } 66 | 67 | div(other: BnLike, cfg = this.#cfg): BN { 68 | return new BN(this.#bn.dividedBy(this._bn(other)), cfg) 69 | } 70 | 71 | add(other: BnLike, cfg = this.#cfg): BN { 72 | return new BN(this.#bn.plus(this._bn(other)), cfg) 73 | } 74 | 75 | sub(other: BnLike, cfg = this.#cfg): BN { 76 | return new BN(this.#bn.minus(this._bn(other)), cfg) 77 | } 78 | 79 | pow(other: BnLike, cfg = this.#cfg): BN { 80 | return new BN(this.#bn.pow(this._bn(other)), cfg) 81 | } 82 | 83 | /** 84 | * this > other => 1; 85 | * this < other => -1; 86 | * this === other => 0; 87 | * 88 | * @param {BnLike} other 89 | * @returns {number} 90 | */ 91 | compare(other: BnLike): number { 92 | return this.#bn.comparedTo(this._bn(other)) 93 | } 94 | 95 | round(precision: number, mode?: BN_ROUNDING): string { 96 | return this.#bn.toPrecision(precision, mode) 97 | } 98 | 99 | format(format?: BnFormatCfg): string { 100 | try { 101 | const { 102 | decimals = BigNumber.config({}).DECIMAL_PLACES as number, 103 | rounding = BigNumber.config({}).ROUNDING_MODE as BN_ROUNDING, 104 | noGroupSeparator, 105 | ...fmt 106 | } = format || {} 107 | const groupSeparatorFormat: { [key: string]: string | number } = { 108 | ...(fmt.groupSeparator 109 | ? { groupSeparator: fmt.groupSeparator as string } 110 | : {}), 111 | } 112 | if (noGroupSeparator) { 113 | groupSeparatorFormat.groupSeparator = '' 114 | } 115 | Object.assign(fmt, BigNumber.config({}).FORMAT, groupSeparatorFormat) 116 | return this.#bn.toFormat(decimals, rounding, fmt) 117 | } catch (error) { 118 | console.error(error) 119 | return '—' 120 | } 121 | } 122 | 123 | toFraction(decimals?: number): BN { 124 | const fr = decimals ? new BN(10).pow(decimals) : BN.TO_WEI_FACTOR 125 | return this.mul(fr) 126 | } 127 | 128 | fromFraction(decimals?: number): BN { 129 | const fr = decimals ? new BN(0.1).pow(decimals) : BN.FROM_WEI_FACTOR 130 | return this.mul(fr) 131 | } 132 | 133 | toWei(): BN { 134 | return this.toFraction() 135 | } 136 | 137 | fromWei(): BN { 138 | return this.fromFraction() 139 | } 140 | 141 | toString(): string { 142 | return this.#bn.toFormat({ 143 | groupSeparator: '', 144 | decimalSeparator: '.', 145 | fractionGroupSeparator: '', 146 | }) 147 | } 148 | 149 | toJSON(): string { 150 | return this.toString() 151 | } 152 | 153 | valueOf(): string { 154 | return this.toString() 155 | } 156 | 157 | private _bn(value: BnLike, config?: BnCfg): BigNumber { 158 | let ctor = BigNumber 159 | if (config) { 160 | ctor = ctor.clone() 161 | ctor.config({ 162 | ...('decimals' in config ? { DECIMAL_PLACES: config.decimals } : {}), 163 | ...('rounding' in config ? { ROUNDING_MODE: config.rounding } : {}), 164 | }) 165 | } 166 | 167 | if (BigNumber.isBigNumber(value)) { 168 | return value 169 | } 170 | 171 | if (value instanceof BN) { 172 | return value.#bn 173 | } 174 | 175 | try { 176 | return new ctor(value) 177 | } catch (error) { 178 | throw new TypeError(`Cannot convert the given "${value}" to BN!`) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /packages/json-api-client/src/response.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PARSED_RESPONSE, 3 | RAW_RESPONSE, 4 | WITHOUT_LINKS_RAW_RESPONSE, 5 | MockWrapper, 6 | } from './test' 7 | import { parseJsonApiResponse } from './middlewares' 8 | import { JsonApiClient } from './json-api' 9 | import { JsonApiLinkFields } from './types' 10 | 11 | jest.mock('./json-api') 12 | 13 | describe('JsonApi response data parsing unit test', () => { 14 | let mockedApi: jest.Mocked 15 | 16 | beforeEach(() => { 17 | jest.resetModules() 18 | jest.clearAllMocks() 19 | 20 | mockedApi = MockWrapper.getMockedApi() 21 | }) 22 | 23 | test('should return correctly parsed response', () => { 24 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 25 | const response = parseJsonApiResponse({ 26 | raw: rawResponse, 27 | isNeedRaw: false, 28 | apiClient: mockedApi, 29 | withCredentials: true, 30 | }) 31 | expect(response.data).toStrictEqual(PARSED_RESPONSE) 32 | }) 33 | 34 | test('should return undefined if data is empty', () => { 35 | const rawResponse = MockWrapper.makeAxiosResponse({}) 36 | const response = parseJsonApiResponse({ 37 | raw: rawResponse, 38 | isNeedRaw: false, 39 | apiClient: mockedApi, 40 | withCredentials: true, 41 | }) 42 | expect(response.data).toBeUndefined() 43 | }) 44 | 45 | test('should have correct raw response and raw data', () => { 46 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 47 | const response = parseJsonApiResponse({ 48 | raw: rawResponse, 49 | isNeedRaw: false, 50 | apiClient: mockedApi, 51 | withCredentials: true, 52 | }) 53 | 54 | expect(response.rawResponse).toStrictEqual(rawResponse) 55 | expect(response.rawData).toStrictEqual(rawResponse.data) 56 | }) 57 | 58 | test('should create correct link from response', () => { 59 | const { JsonApiClient } = jest.requireActual('./json-api') 60 | 61 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 62 | 63 | const api = new JsonApiClient({ baseUrl: 'https://localhost:8095/core' }) 64 | 65 | const response = parseJsonApiResponse({ 66 | raw: rawResponse, 67 | isNeedRaw: false, 68 | apiClient: api, 69 | withCredentials: true, 70 | }) 71 | 72 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 73 | // @ts-ignore 74 | expect(response._createLink(response.links.next)).toBe( 75 | '/meta-buy-orders?filter%5Bowner%5D=3d38fff3-847f-45f2-a267-891ba90dac37&include=meta_sell_order%2Cmeta_sell_order.token%2Cmeta_sell_order.token.metadata&page%5Blimit%5D=5&page%5Bnumber%5D=1&page%5Border%5D=desc', 76 | ) 77 | }) 78 | 79 | test('should return raw response', () => { 80 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 81 | const response = parseJsonApiResponse({ 82 | raw: rawResponse, 83 | isNeedRaw: true, 84 | apiClient: mockedApi, 85 | withCredentials: true, 86 | }) 87 | 88 | expect(response.data).toStrictEqual(rawResponse.data) 89 | }) 90 | 91 | test('should have links object', () => { 92 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE) 93 | const response = parseJsonApiResponse({ 94 | raw: rawResponse, 95 | isNeedRaw: false, 96 | apiClient: mockedApi, 97 | withCredentials: true, 98 | }) 99 | 100 | expect(response.links).toBeDefined() 101 | expect(response.isLinksExist).toBeTruthy() 102 | }) 103 | 104 | test('should have valid page limit', () => { 105 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE, 200, { 106 | params: { 107 | 'page[limit]': 100, 108 | }, 109 | }) 110 | 111 | const response = parseJsonApiResponse({ 112 | raw: rawResponse, 113 | isNeedRaw: false, 114 | apiClient: mockedApi, 115 | withCredentials: true, 116 | }) 117 | 118 | expect(response.pageLimit).toBe(100) 119 | }) 120 | 121 | test('should throw exception if "links" is empty', () => { 122 | const rawResponse = MockWrapper.makeAxiosResponse( 123 | WITHOUT_LINKS_RAW_RESPONSE, 124 | ) 125 | 126 | const response = parseJsonApiResponse({ 127 | raw: rawResponse, 128 | isNeedRaw: false, 129 | apiClient: mockedApi, 130 | withCredentials: true, 131 | }) 132 | 133 | expect(response.fetchPage(JsonApiLinkFields.next)).rejects.toThrow( 134 | 'There are no links in response', 135 | ) 136 | }) 137 | 138 | test('should fetch next page', async () => { 139 | const rawResponse = MockWrapper.makeAxiosResponse(RAW_RESPONSE, 200, { 140 | method: 'GET', 141 | headers: {}, 142 | }) 143 | 144 | const response = parseJsonApiResponse({ 145 | raw: rawResponse, 146 | isNeedRaw: false, 147 | apiClient: mockedApi, 148 | withCredentials: true, 149 | }) 150 | 151 | mockedApi.request.mockImplementationOnce( 152 | () => new Promise(resolve => resolve(response)), 153 | ) 154 | 155 | await response.fetchPage(JsonApiLinkFields.next) 156 | 157 | expect(mockedApi.request).toBeCalledWith({ 158 | endpoint: response.links.next, 159 | method: 'GET', 160 | headers: {}, 161 | isNeedRaw: false, 162 | withCredentials: true, 163 | }) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /packages/utils/src/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs } from 'dayjs' 2 | import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' 3 | import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' 4 | import relativeTime from 'dayjs/plugin/relativeTime' 5 | import isBetween from 'dayjs/plugin/isBetween' 6 | import calendar from 'dayjs/plugin/calendar' 7 | import utc from 'dayjs/plugin/utc' 8 | import customParseFormat from 'dayjs/plugin/customParseFormat' 9 | import updateLocale from 'dayjs/plugin/updateLocale' 10 | import timezone from 'dayjs/plugin/timezone' 11 | import duration from 'dayjs/plugin/duration' 12 | import { 13 | IsoDate, 14 | UnixDate, 15 | Inclusivity, 16 | TimeDate, 17 | TimeFormat, 18 | TimeOpUnit, 19 | TimeUnit, 20 | TimeManipulate, 21 | TimeCalendar, 22 | TimeLocale, 23 | } from '@/types' 24 | 25 | dayjs.extend(isSameOrAfter) 26 | dayjs.extend(isSameOrBefore) 27 | dayjs.extend(relativeTime) 28 | dayjs.extend(isBetween) 29 | dayjs.extend(calendar) 30 | dayjs.extend(utc) 31 | dayjs.extend(customParseFormat) 32 | dayjs.extend(updateLocale) 33 | dayjs.extend(timezone) 34 | dayjs.extend(duration) 35 | 36 | export class Time { 37 | #date: Dayjs 38 | 39 | constructor(date?: TimeDate, format?: TimeFormat) { 40 | this.#date = this._dayjs(date, format) 41 | } 42 | 43 | public static locale( 44 | preset?: string | ILocale, 45 | object?: Partial, 46 | isLocal?: boolean, 47 | ): string { 48 | return dayjs.locale(preset, object, isLocal) 49 | } 50 | 51 | public static setDefaultTimezone(timezone?: string): void { 52 | dayjs.tz.setDefault(timezone) 53 | } 54 | 55 | public static setLocale( 56 | localeName: string, 57 | customConfig: TimeLocale, 58 | ): TimeLocale { 59 | return dayjs.updateLocale(localeName, customConfig) 60 | } 61 | 62 | private _dayjs(date?: TimeDate, format?: TimeFormat): Dayjs { 63 | return dayjs(date, format) 64 | } 65 | 66 | private _tz(date: TimeDate, timezone?: string) { 67 | return dayjs.tz(date, timezone) 68 | } 69 | 70 | public get dayjs(): Dayjs { 71 | return this.#date 72 | } 73 | 74 | public tz(timezone?: string): Time { 75 | this.#date = this._tz(this.#date, timezone) 76 | return this 77 | } 78 | 79 | public utc(keepLocalTime?: boolean): Time { 80 | this.#date = this.#date.utc(keepLocalTime) 81 | return this 82 | } 83 | 84 | public get isValid(): boolean { 85 | return this.#date.isValid() 86 | } 87 | 88 | public clone(): Time { 89 | return new Time(this.#date.clone()) 90 | } 91 | 92 | public get timestamp(): UnixDate { 93 | return this.#date.unix() 94 | } 95 | 96 | public get ms(): number { 97 | return this.#date.valueOf() 98 | } 99 | 100 | public get ISO(): IsoDate { 101 | return this.#date.toISOString() 102 | } 103 | 104 | public get RFC3339(): IsoDate { 105 | return this.#date.utc(true).format('YYYY-MM-DDTHH:mm:ss[Z]') 106 | } 107 | 108 | public get(unit: TimeUnit): number { 109 | return this.#date.get(unit) 110 | } 111 | 112 | public getAsObject(unit: TimeUnit[]): { 113 | [K in typeof unit[number]]: number 114 | } { 115 | return unit.reduce( 116 | (acc, item) => { 117 | acc[item] = this.get(item) 118 | 119 | return acc 120 | }, 121 | {} as { 122 | [K in typeof unit[number]]: number 123 | }, 124 | ) 125 | } 126 | 127 | public add(value: number, unit?: TimeManipulate): Time { 128 | this.#date = this.#date.add(value, unit) 129 | return this 130 | } 131 | 132 | public format(format?: string): IsoDate { 133 | return this.#date.format(format) 134 | } 135 | 136 | public toDate(): Date { 137 | return this.#date.toDate() 138 | } 139 | 140 | public toCalendar(referenceTime?: TimeDate, calendar?: TimeCalendar): string { 141 | return this.#date.calendar(referenceTime, calendar) 142 | } 143 | 144 | public subtract(value: number, unit?: TimeManipulate): Time { 145 | this.#date = this.#date.subtract(value, unit) 146 | return this 147 | } 148 | 149 | public startOf(unit: TimeOpUnit): Time { 150 | this.#date = this.#date.startOf(unit) 151 | return this 152 | } 153 | 154 | public isSame(comparisonDate?: TimeDate, unit?: TimeOpUnit): boolean { 155 | return this.#date.isSame(comparisonDate, unit) 156 | } 157 | 158 | public isBefore(comparisonDate?: TimeDate): boolean { 159 | return this.#date.isBefore(comparisonDate) 160 | } 161 | 162 | public isAfter(comparisonDate?: TimeDate): boolean { 163 | return this.#date.isAfter(comparisonDate) 164 | } 165 | 166 | public isSameOrAfter(comparisonDate?: TimeDate): boolean { 167 | return this.#date.isSameOrAfter(comparisonDate) 168 | } 169 | 170 | public isSameOrBefore(comparisonDate?: TimeDate): boolean { 171 | return this.#date.isSameOrBefore(comparisonDate) 172 | } 173 | 174 | public isBetween( 175 | startDate?: TimeDate, 176 | endDate?: TimeDate, 177 | unit?: TimeManipulate, 178 | inclusivity?: Inclusivity, 179 | ): boolean { 180 | return this.#date.isBetween(startDate, endDate, unit, inclusivity) 181 | } 182 | 183 | public diff( 184 | comparisonDate: Time, 185 | unit?: TimeUnit, 186 | isTruncated = false, 187 | ): number { 188 | return this.#date.diff(comparisonDate.dayjs, unit, isTruncated) 189 | } 190 | 191 | public getFrom(date: TimeDate): string { 192 | return this.#date.from(date) 193 | } 194 | 195 | public get fromNow(): string { 196 | return this.#date.fromNow() 197 | } 198 | 199 | public getTo(date: TimeDate): string { 200 | return this.#date.to(date) 201 | } 202 | 203 | public get toNow(): string { 204 | return this.#date.toNow() 205 | } 206 | } 207 | 208 | export const time = (date: TimeDate, format?: TimeFormat): Time => 209 | new Time(date, format) 210 | -------------------------------------------------------------------------------- /packages/json-api-client/README.md: -------------------------------------------------------------------------------- 1 | # @distributedlab/json-api-client 2 | JSON API client 3 | 4 | ![version (scoped package)](https://badgen.net/npm/v/@distributedlab/json-api-client) 5 | ![types](https://badgen.net/npm/types/@distributedlab/json-api-client) 6 | ![tree-shaking](https://badgen.net/bundlephobia/tree-shaking/@distributedlab/json-api-client) 7 | ![checks](https://badgen.net/github/checks/distributed-lab/web-kit-old/main) 8 | 9 | ### This package moved to the [new repository](https://github.com/distributed-lab/web-kit) and not be maintained anymore. 10 | 11 | ## Usage 12 | #### Bearer token 13 | ```typescript 14 | // interceptors.ts 15 | import { HTTPS_STATUS_CODES } from '@distributedlab/json-api-client' 16 | import { AxiosInstance, AxiosRequestConfig } from 'axios' 17 | import { useAuthStore } from '@/store' 18 | import { router } from '@/router' 19 | import { Bus } from '@/utils' 20 | import { ROUTE_NAMES } from '@/enums' 21 | import { useI18n } from '@/localization' 22 | 23 | export function attachBearerInjector(axios: AxiosInstance): void { 24 | axios.interceptors.request.use((request): AxiosRequestConfig => { 25 | // Some authentication store in the client app 26 | const authStore = useAuthStore() 27 | if (!authStore.accessToken) return request 28 | 29 | if (!request.headers) request.headers = {} 30 | // Attach bearer token to every request 31 | request.headers['Authorization'] = `Bearer ${authStore.accessToken}` 32 | return request 33 | }) 34 | } 35 | 36 | export function attachStaleTokenHandler(axios: AxiosInstance): void { 37 | axios.interceptors.response.use( 38 | response => response, 39 | async error => { 40 | const config = error?.config 41 | const isUnauthorized = ( 42 | error?.response?.status === HTTPS_STATUS_CODES.UNAUTHORIZED && 43 | !config?._retry 44 | ) 45 | 46 | // If error isn't unauthorized or request was already retried - return error 47 | if (!isUnauthorized) return Promise.reject(error) 48 | 49 | // Some authentication store in the client app 50 | const authStore = useAuthStore() 51 | const { $t } = useI18n() 52 | 53 | try { 54 | config._retry = true 55 | // Executes some refresh token logic in the client app 56 | await authStore.refreshToken() 57 | 58 | // Reset default axios authorization header witn new token 59 | axios.defaults.headers.common['Authorization'] = `Bearer ${authStore.accessToken}` 60 | 61 | return axios(config) 62 | } catch (_error) { 63 | 64 | /** Example of handling refresh token error in the client app 65 | * 66 | * Implementation may differ from example 67 | * 68 | * We can logout user and redirect him to the login page and 69 | * emit bus error event to show user that session expired 70 | */ 71 | authStore.logout() 72 | router.push({ name: ROUTE_NAMES.login }) 73 | Bus.error({ 74 | title: $t('api-errors.session-expired-title'), 75 | message: $t('api-errors.session-expired-desc'), 76 | }) 77 | return Promise.reject(_error) 78 | } 79 | }, 80 | ) 81 | } 82 | 83 | // api.ts 84 | import { JsonApiClient } from '@distributedlab/json-api-client'; 85 | import { attachBearerInjector, attachStaleTokenHandler } from '@/interceptors'; 86 | 87 | const axiosInstance = axios.create() 88 | attachBearerInjector(axiosInstance) 89 | attachStaleTokenHandler(axiosInstance) 90 | 91 | export const api = new JsonApiClient({ 92 | baseUrl: 'https://api.example.com', 93 | axios: axiosInstance, 94 | }); 95 | ``` 96 | 97 | ## Changelog 98 | All notable changes to this project will be documented in this file. 99 | 100 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 101 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 102 | 103 |
2.2.1 104 |

Change

105 |
    106 |
  • Deprecate package
  • 107 |
108 |
109 |
2.2.0 110 |

Added

111 |
    112 |
  • Meta typing in response
  • 113 |
114 |
115 |
2.1.1 116 |

Fixed

117 |
    118 |
  • Exporting JsonApiBodyBuilder from package
  • 119 |
120 |
121 |
2.1.0 122 |

Added

123 |
    124 |
  • Util that helps to create the body for POST requests
  • 125 |
126 |
127 |
2.0.4 128 |

Removed

129 |
    130 |
  • Axios paramsSerializer encode config
  • 131 |
132 |
133 |
2.0.3 134 |

Changed

135 |
    136 |
  • Updated axios to 1.0.0
  • 137 |
138 |
139 |
2.0.2 140 |

Added

141 |
    142 |
  • Export helpers, enums and types
  • 143 |
144 |
145 |
2.0.1 146 |

Fixed

147 |
    148 |
  • Build content in NPM package
  • 149 |
150 |
151 |
2.0.0 152 |

Added

153 |
    154 |
  • 155 | Ability to provide axios instance to make possible to inject interceptors 156 | from client code to handle authorization and refresh token logic 157 |
  • 158 |
159 |

Removed

160 |
    161 |
  • Ability to provide auth token
  • 162 |
163 |
164 |
1.0.2 165 |

Fixed

166 |
    167 |
  • @babel/runtime dependency
  • 168 |
169 |
170 |
1.0.1 171 |

Fixed

172 |
    173 |
  • Readme
  • 174 |
175 |
176 |
1.0.0 177 |

Under the hood changes

178 |
    179 |
  • Initiated project
  • 180 |
181 |
182 | 183 | -------------------------------------------------------------------------------- /packages/json-api-client/src/json-api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosInstance } from 'axios' 2 | import { HTTP_METHODS } from './enums' 3 | import { JsonApiResponse } from '@/response' 4 | import { 5 | JsonApiClientConfig, 6 | JsonApiClientRequestConfig, 7 | JsonApiClientRequestOpts, 8 | JsonApiDefaultMeta, 9 | JsonApiResponseErrors, 10 | URL, 11 | } from './types' 12 | import { 13 | flattenToAxiosJsonApiQuery, 14 | parseJsonApiError, 15 | parseJsonApiResponse, 16 | setJsonApiHeaders, 17 | } from './middlewares' 18 | import { isUndefined } from './helpers' 19 | 20 | /** 21 | * Represents JsonApiClient that performs requests to backend 22 | */ 23 | export class JsonApiClient { 24 | private _baseUrl: URL 25 | private _axios: AxiosInstance 26 | 27 | constructor(config = {} as JsonApiClientConfig) { 28 | this._baseUrl = config.baseUrl ?? '' 29 | this._axios = config.axios ?? axios.create() 30 | } 31 | 32 | /** 33 | * Clones current JsonApiClient instance 34 | */ 35 | private _clone(): JsonApiClient { 36 | return Object.assign(Object.create(Object.getPrototypeOf(this)), this) 37 | } 38 | 39 | /** 40 | * Sets axios instance to the client instance. 41 | */ 42 | public get axios(): AxiosInstance { 43 | return this._axios 44 | } 45 | 46 | /** 47 | * Sets axios instance to the client instance. 48 | */ 49 | public useAxios(axiosInstance: AxiosInstance): JsonApiClient { 50 | this._axios = axiosInstance 51 | return this 52 | } 53 | 54 | /** 55 | * Base URL will be prepended to `url` unless `url` is absolute. 56 | * It can be convenient to set `baseURL` for an instance of axios to pass 57 | * relative URLs to methods of that instance. 58 | * 59 | * For more details look Axios Request config: 60 | * {@link https://github.com/axios/axios#request-config} 61 | */ 62 | public get baseUrl(): URL { 63 | return this._baseUrl 64 | } 65 | 66 | /** 67 | * Assigns new base URL to the current instance. 68 | */ 69 | useBaseUrl(baseUrl: URL): JsonApiClient { 70 | if (!baseUrl) throw new TypeError('Arg "baseUrl" not passed') 71 | this._baseUrl = baseUrl 72 | return this 73 | } 74 | 75 | /** 76 | * Creates new instance JsonApiClient instance with given base URL. 77 | */ 78 | withBaseUrl(baseUrl: URL): JsonApiClient { 79 | if (!baseUrl) throw new TypeError('Arg "baseUrl" not passed') 80 | 81 | return this._clone().useBaseUrl(baseUrl) 82 | } 83 | 84 | /** 85 | * Performs a http request 86 | */ 87 | async request( 88 | opts: JsonApiClientRequestOpts, 89 | ): Promise> { 90 | let response 91 | 92 | const config: JsonApiClientRequestConfig = { 93 | baseURL: this.baseUrl, 94 | params: opts.query ?? {}, 95 | data: opts.isEmptyBodyAllowed && !opts.data ? undefined : opts.data || {}, 96 | method: opts.method, 97 | headers: opts?.headers ?? {}, 98 | url: opts.endpoint, 99 | withCredentials: isUndefined(opts.withCredentials) 100 | ? true 101 | : opts.withCredentials, 102 | maxContentLength: 100000000000, 103 | maxBodyLength: 1000000000000, 104 | } 105 | 106 | config.params = flattenToAxiosJsonApiQuery(config) 107 | 108 | if (!opts.headers) { 109 | config.headers = setJsonApiHeaders(config) 110 | 111 | if (opts.contentType) config.headers['Content-Type'] = opts.contentType 112 | } 113 | 114 | try { 115 | response = await this._axios(config) 116 | } catch (e) { 117 | throw parseJsonApiError(e as AxiosError) 118 | } 119 | 120 | return parseJsonApiResponse({ 121 | raw: response, 122 | isNeedRaw: Boolean(opts?.isNeedRaw), 123 | apiClient: this, 124 | withCredentials: Boolean(opts?.withCredentials), 125 | }) 126 | } 127 | 128 | /** 129 | * Makes a `GET` to a target `endpoint` with the provided `query` params. 130 | * Parses the response in JsonApi format. 131 | */ 132 | get( 133 | endpoint: string, 134 | query: Record = {}, 135 | isNeedRaw?: boolean, 136 | ): Promise> { 137 | return this.request({ 138 | method: HTTP_METHODS.GET, 139 | endpoint, 140 | query, 141 | isEmptyBodyAllowed: true, 142 | isNeedRaw, 143 | }) 144 | } 145 | 146 | /** 147 | * Makes a `POST` to a target `endpoint` with the provided `data` as body. 148 | * Parses the response in JsonApi format. 149 | */ 150 | post( 151 | endpoint: string, 152 | data: unknown, 153 | isNeedRaw?: boolean, 154 | ): Promise> { 155 | return this.request({ 156 | method: HTTP_METHODS.POST, 157 | endpoint, 158 | data, 159 | isNeedRaw, 160 | }) 161 | } 162 | 163 | /** 164 | * Makes a `PATCH` to a target `endpoint` with the provided `data` as body. 165 | * Signing can be enabled with `needSign` argument. Parses the response in 166 | * JsonApi format. 167 | */ 168 | patch( 169 | endpoint: string, 170 | data?: unknown, 171 | ): Promise> { 172 | return this.request({ 173 | method: HTTP_METHODS.PATCH, 174 | endpoint, 175 | data, 176 | }) 177 | } 178 | 179 | /** 180 | * Makes a `PUT` to a target `endpoint` with the provided `data` as body. 181 | * Parses the response in JsonApi format. 182 | */ 183 | put( 184 | endpoint: string, 185 | data: unknown, 186 | ): Promise> { 187 | return this.request({ 188 | method: HTTP_METHODS.PUT, 189 | endpoint, 190 | data, 191 | }) 192 | } 193 | 194 | /** 195 | * Makes a `DELETE` to a target `endpoint` with the provided `data` as body. 196 | * Parses the response in JsonApi format. 197 | */ 198 | delete( 199 | endpoint: string, 200 | data?: unknown, 201 | ): Promise> { 202 | return this.request({ 203 | method: HTTP_METHODS.DELETE, 204 | endpoint, 205 | data, 206 | isEmptyBodyAllowed: true, 207 | }) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/json-api-client/src/test/mocks/json-api-response-without-links-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "a80fbb70-917c-4593-bcc4-ad536a68389b", 5 | "type": "meta-buy-order", 6 | "attributes": { 7 | "amount": 1, 8 | "created_at": "2022-02-04T13:39:18.830123Z", 9 | "email": "foo@bar.com", 10 | "state": "created", 11 | "verification_url": "https://my-fake-url.com/verification/1212ad13-607d-31e5-86af-73d74917ec83" 12 | }, 13 | "relationships": { 14 | "meta_sell_order": { 15 | "data": { 16 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 17 | "type": "meta-sell-order" 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "id": "9b061c42-6d20-4657-9f7f-db157736c50f", 24 | "type": "meta-buy-order", 25 | "attributes": { 26 | "amount": 1, 27 | "created_at": "2022-02-04T13:39:07.51962Z", 28 | "email": "foo@bar.com", 29 | "state": "created", 30 | "verification_url": "https://my-fake-url.com/verification/328ecdce-4bd8-340e-84f2-35de12180cb3" 31 | }, 32 | "relationships": { 33 | "meta_sell_order": { 34 | "data": { 35 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 36 | "type": "meta-sell-order" 37 | } 38 | } 39 | } 40 | }, 41 | { 42 | "id": "03980a87-9afb-4952-86c6-674eaf555e51", 43 | "type": "meta-buy-order", 44 | "attributes": { 45 | "amount": 1, 46 | "claimable_at": "2022-01-25T14:29:17.230663Z", 47 | "created_at": "2022-02-01T08:39:08.900574Z", 48 | "email": "test@test.com", 49 | "state": "receiver_pending", 50 | "verification_url": "" 51 | }, 52 | "relationships": { 53 | "meta_sell_order": { 54 | "data": { 55 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 56 | "type": "meta-sell-order" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "id": "497314c0-3d99-4a22-aebb-0c60913324da", 63 | "type": "meta-buy-order", 64 | "attributes": { 65 | "amount": 1, 66 | "claimable_at": "2022-01-25T14:29:17.230663Z", 67 | "created_at": "2022-01-25T08:31:18.15616Z", 68 | "email": "test@test.net", 69 | "state": "receiver_pending", 70 | "verification_url": "" 71 | }, 72 | "relationships": { 73 | "meta_sell_order": { 74 | "data": { 75 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 76 | "type": "meta-sell-order" 77 | } 78 | } 79 | } 80 | }, 81 | { 82 | "id": "c5b5ca54-1760-43a5-a8ef-b48aad0e30d7", 83 | "type": "meta-buy-order", 84 | "attributes": { 85 | "amount": 1, 86 | "claimable_at": "2022-01-25T14:29:17.230663Z", 87 | "created_at": "2022-01-25T08:29:07.054527Z", 88 | "email": "test@test.net", 89 | "state": "receiver_pending", 90 | "verification_url": "" 91 | }, 92 | "relationships": { 93 | "meta_sell_order": { 94 | "data": { 95 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 96 | "type": "meta-sell-order" 97 | } 98 | } 99 | } 100 | } 101 | ], 102 | "included": [ 103 | { 104 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 105 | "type": "metadata", 106 | "attributes": { 107 | "attributes": {}, 108 | "created_at": "2021-12-17T13:37:33.965001Z", 109 | "description": "asdasd", 110 | "image_url": "https://my-fake-url.com/example", 111 | "name": "test", 112 | "updated_at": "2021-12-17T13:37:33.965001Z" 113 | }, 114 | "relationships": { 115 | "network": { 116 | "data": { 117 | "id": "testnet", 118 | "type": "networks" 119 | } 120 | }, 121 | "owner": { 122 | "data": { 123 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 124 | "type": "identities" 125 | } 126 | } 127 | } 128 | }, 129 | { 130 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 131 | "type": "metadata", 132 | "attributes": { 133 | "attributes": {}, 134 | "created_at": "2021-12-29T14:36:01.796775Z", 135 | "description": "asd", 136 | "image_url": "https://my-fake-url.com/2b3a17df-932d-4421-b03c-b05cebc7eae", 137 | "name": "response", 138 | "updated_at": "2021-12-29T14:36:01.796775Z" 139 | }, 140 | "relationships": { 141 | "network": { 142 | "data": { 143 | "id": "testnet", 144 | "type": "networks" 145 | } 146 | }, 147 | "owner": { 148 | "data": { 149 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 150 | "type": "identities" 151 | } 152 | } 153 | } 154 | }, 155 | { 156 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 157 | "type": "meta-sell-order", 158 | "attributes": { 159 | "amount": 100, 160 | "available_amount": 71463, 161 | "currency": "USD", 162 | "price": 100, 163 | "views_count": 0 164 | }, 165 | "relationships": { 166 | "owner": { 167 | "data": { 168 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 169 | "type": "identities" 170 | } 171 | }, 172 | "token": { 173 | "data": { 174 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 175 | "type": "tokens" 176 | } 177 | } 178 | } 179 | }, 180 | { 181 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 182 | "type": "meta-sell-order", 183 | "attributes": { 184 | "amount": 10, 185 | "available_amount": 2, 186 | "currency": "USD", 187 | "price": 200, 188 | "views_count": 0 189 | }, 190 | "relationships": { 191 | "owner": { 192 | "data": { 193 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 194 | "type": "identities" 195 | } 196 | }, 197 | "token": { 198 | "data": { 199 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 200 | "type": "tokens" 201 | } 202 | } 203 | } 204 | }, 205 | { 206 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 207 | "type": "tokens", 208 | "attributes": { 209 | "created_at": "2021-12-17T13:37:34.508321Z", 210 | "external_id": "0x1a725171cbe7dd52eecb2d1c7b7e23b991608bcdaf53f36b72dcedbc77ce27ff", 211 | "updated_at": "2021-12-17T13:37:34.508321Z" 212 | }, 213 | "relationships": { 214 | "contract": { 215 | "data": { 216 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b", 217 | "type": "contracts" 218 | } 219 | }, 220 | "metadata": { 221 | "data": { 222 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 223 | "type": "metadata" 224 | } 225 | } 226 | } 227 | }, 228 | { 229 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 230 | "type": "tokens", 231 | "attributes": { 232 | "created_at": "2021-12-29T14:36:02.065904Z", 233 | "external_id": "0x5d40545cdc1cd0cc9961c2561e531e8a9ed0a9b44531b249dcd931bf1db09a", 234 | "updated_at": "2021-12-29T14:36:02.065904Z" 235 | }, 236 | "relationships": { 237 | "contract": { 238 | "data": { 239 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b", 240 | "type": "contracts" 241 | } 242 | }, 243 | "metadata": { 244 | "data": { 245 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 246 | "type": "metadata" 247 | } 248 | } 249 | } 250 | } 251 | ] 252 | } 253 | -------------------------------------------------------------------------------- /packages/json-api-client/src/test/mocks/json-api-response-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "a80fbb70-917c-4593-bcc4-ad536a68389b", 5 | "type": "meta-buy-order", 6 | "attributes": { 7 | "amount": 1, 8 | "created_at": "2022-02-04T13:39:18.830123Z", 9 | "email": "foo@bar.com", 10 | "state": "created", 11 | "verification_url": "https://my-fake-url.com/verification/1212ad13-607d-31e5-86af-73d74917ec83" 12 | }, 13 | "relationships": { 14 | "meta_sell_order": { 15 | "data": { 16 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 17 | "type": "meta-sell-order" 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "id": "9b061c42-6d20-4657-9f7f-db157736c50f", 24 | "type": "meta-buy-order", 25 | "attributes": { 26 | "amount": 1, 27 | "created_at": "2022-02-04T13:39:07.51962Z", 28 | "email": "foo@bar.com", 29 | "state": "created", 30 | "verification_url": "https://my-fake-url.com/verification/328ecdce-4bd8-340e-84f2-35de12180cb3" 31 | }, 32 | "relationships": { 33 | "meta_sell_order": { 34 | "data": { 35 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 36 | "type": "meta-sell-order" 37 | } 38 | } 39 | } 40 | }, 41 | { 42 | "id": "03980a87-9afb-4952-86c6-674eaf555e51", 43 | "type": "meta-buy-order", 44 | "attributes": { 45 | "amount": 1, 46 | "claimable_at": "2022-01-25T14:29:17.230663Z", 47 | "created_at": "2022-02-01T08:39:08.900574Z", 48 | "email": "test@test.com", 49 | "state": "receiver_pending", 50 | "verification_url": "" 51 | }, 52 | "relationships": { 53 | "meta_sell_order": { 54 | "data": { 55 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 56 | "type": "meta-sell-order" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "id": "497314c0-3d99-4a22-aebb-0c60913324da", 63 | "type": "meta-buy-order", 64 | "attributes": { 65 | "amount": 1, 66 | "claimable_at": "2022-01-25T14:29:17.230663Z", 67 | "created_at": "2022-01-25T08:31:18.15616Z", 68 | "email": "test@test.net", 69 | "state": "receiver_pending", 70 | "verification_url": "" 71 | }, 72 | "relationships": { 73 | "meta_sell_order": { 74 | "data": { 75 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 76 | "type": "meta-sell-order" 77 | } 78 | } 79 | } 80 | }, 81 | { 82 | "id": "c5b5ca54-1760-43a5-a8ef-b48aad0e30d7", 83 | "type": "meta-buy-order", 84 | "attributes": { 85 | "amount": 1, 86 | "claimable_at": "2022-01-25T14:29:17.230663Z", 87 | "created_at": "2022-01-25T08:29:07.054527Z", 88 | "email": "test@test.net", 89 | "state": "receiver_pending", 90 | "verification_url": "" 91 | }, 92 | "relationships": { 93 | "meta_sell_order": { 94 | "data": { 95 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 96 | "type": "meta-sell-order" 97 | } 98 | } 99 | } 100 | } 101 | ], 102 | "included": [ 103 | { 104 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 105 | "type": "metadata", 106 | "attributes": { 107 | "attributes": {}, 108 | "created_at": "2021-12-17T13:37:33.965001Z", 109 | "description": "asdasd", 110 | "image_url": "https://my-fake-url.com/example", 111 | "name": "test", 112 | "updated_at": "2021-12-17T13:37:33.965001Z" 113 | }, 114 | "relationships": { 115 | "network": { 116 | "data": { 117 | "id": "testnet", 118 | "type": "networks" 119 | } 120 | }, 121 | "owner": { 122 | "data": { 123 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 124 | "type": "identities" 125 | } 126 | } 127 | } 128 | }, 129 | { 130 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 131 | "type": "metadata", 132 | "attributes": { 133 | "attributes": {}, 134 | "created_at": "2021-12-29T14:36:01.796775Z", 135 | "description": "asd", 136 | "image_url": "https://my-fake-url.com/2b3a17df-932d-4421-b03c-b05cebc7eae", 137 | "name": "response", 138 | "updated_at": "2021-12-29T14:36:01.796775Z" 139 | }, 140 | "relationships": { 141 | "network": { 142 | "data": { 143 | "id": "testnet", 144 | "type": "networks" 145 | } 146 | }, 147 | "owner": { 148 | "data": { 149 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 150 | "type": "identities" 151 | } 152 | } 153 | } 154 | }, 155 | { 156 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 157 | "type": "meta-sell-order", 158 | "attributes": { 159 | "amount": 100, 160 | "available_amount": 71463, 161 | "currency": "USD", 162 | "price": 100, 163 | "views_count": 0 164 | }, 165 | "relationships": { 166 | "owner": { 167 | "data": { 168 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 169 | "type": "identities" 170 | } 171 | }, 172 | "token": { 173 | "data": { 174 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 175 | "type": "tokens" 176 | } 177 | } 178 | } 179 | }, 180 | { 181 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 182 | "type": "meta-sell-order", 183 | "attributes": { 184 | "amount": 10, 185 | "available_amount": 2, 186 | "currency": "USD", 187 | "price": 200, 188 | "views_count": 0 189 | }, 190 | "relationships": { 191 | "owner": { 192 | "data": { 193 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37", 194 | "type": "identities" 195 | } 196 | }, 197 | "token": { 198 | "data": { 199 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 200 | "type": "tokens" 201 | } 202 | } 203 | } 204 | }, 205 | { 206 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 207 | "type": "tokens", 208 | "attributes": { 209 | "created_at": "2021-12-17T13:37:34.508321Z", 210 | "external_id": "0x1a725171cbe7dd52eecb2d1c7b7e23b991608bcdaf53f36b72dcedbc77ce27ff", 211 | "updated_at": "2021-12-17T13:37:34.508321Z" 212 | }, 213 | "relationships": { 214 | "contract": { 215 | "data": { 216 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b", 217 | "type": "contracts" 218 | } 219 | }, 220 | "metadata": { 221 | "data": { 222 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 223 | "type": "metadata" 224 | } 225 | } 226 | } 227 | }, 228 | { 229 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 230 | "type": "tokens", 231 | "attributes": { 232 | "created_at": "2021-12-29T14:36:02.065904Z", 233 | "external_id": "0x5d40545cdc1cd0cc9961c2561e531e8a9ed0a9b44531b249dcd931bf1db09a", 234 | "updated_at": "2021-12-29T14:36:02.065904Z" 235 | }, 236 | "relationships": { 237 | "contract": { 238 | "data": { 239 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b", 240 | "type": "contracts" 241 | } 242 | }, 243 | "metadata": { 244 | "data": { 245 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 246 | "type": "metadata" 247 | } 248 | } 249 | } 250 | } 251 | ], 252 | "links": { 253 | "first": "", 254 | "last": "", 255 | "next": "/core/meta-buy-orders?filter%5Bowner%5D=3d38fff3-847f-45f2-a267-891ba90dac37&include=meta_sell_order%2Cmeta_sell_order.token%2Cmeta_sell_order.token.metadata&page%5Blimit%5D=5&page%5Bnumber%5D=1&page%5Border%5D=desc", 256 | "prev": "", 257 | "self": "/core/meta-buy-orders?filter%5Bowner%5D=3d38fff3-847f-45f2-a267-891ba90dac37&include=meta_sell_order%2Cmeta_sell_order.token%2Cmeta_sell_order.token.metadata&page%5Blimit%5D=5&page%5Bnumber%5D=0&page%5Border%5D=desc" 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /packages/json-api-client/src/test/mocks/json-api-response-parsed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "meta-buy-order", 4 | "id": "a80fbb70-917c-4593-bcc4-ad536a68389b", 5 | "amount": 1, 6 | "created_at": "2022-02-04T13:39:18.830123Z", 7 | "email": "foo@bar.com", 8 | "state": "created", 9 | "verification_url": "https://my-fake-url.com/verification/1212ad13-607d-31e5-86af-73d74917ec83", 10 | "meta_sell_order": { 11 | "type": "meta-sell-order", 12 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 13 | "amount": 100, 14 | "available_amount": 71463, 15 | "currency": "USD", 16 | "price": 100, 17 | "views_count": 0, 18 | "owner": { 19 | "type": "identities", 20 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 21 | }, 22 | "token": { 23 | "type": "tokens", 24 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 25 | "created_at": "2021-12-17T13:37:34.508321Z", 26 | "external_id": "0x1a725171cbe7dd52eecb2d1c7b7e23b991608bcdaf53f36b72dcedbc77ce27ff", 27 | "updated_at": "2021-12-17T13:37:34.508321Z", 28 | "contract": { 29 | "type": "contracts", 30 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b" 31 | }, 32 | "metadata": { 33 | "type": "metadata", 34 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 35 | "attributes": {}, 36 | "created_at": "2021-12-17T13:37:33.965001Z", 37 | "description": "asdasd", 38 | "image_url": "https://my-fake-url.com/example", 39 | "name": "test", 40 | "updated_at": "2021-12-17T13:37:33.965001Z", 41 | "network": { 42 | "type": "networks", 43 | "id": "testnet" 44 | }, 45 | "owner": { 46 | "type": "identities", 47 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 48 | }, 49 | "relationshipNames": [ 50 | "network", 51 | "owner" 52 | ] 53 | }, 54 | "relationshipNames": [ 55 | "contract", 56 | "metadata" 57 | ] 58 | }, 59 | "relationshipNames": [ 60 | "owner", 61 | "token" 62 | ] 63 | }, 64 | "relationshipNames": [ 65 | "meta_sell_order" 66 | ] 67 | }, 68 | { 69 | "type": "meta-buy-order", 70 | "id": "9b061c42-6d20-4657-9f7f-db157736c50f", 71 | "amount": 1, 72 | "created_at": "2022-02-04T13:39:07.51962Z", 73 | "email": "foo@bar.com", 74 | "state": "created", 75 | "verification_url": "https://my-fake-url.com/verification/328ecdce-4bd8-340e-84f2-35de12180cb3", 76 | "meta_sell_order": { 77 | "type": "meta-sell-order", 78 | "id": "b34bdfff-029e-416b-b7dd-1271f588130b", 79 | "amount": 100, 80 | "available_amount": 71463, 81 | "currency": "USD", 82 | "price": 100, 83 | "views_count": 0, 84 | "owner": { 85 | "type": "identities", 86 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 87 | }, 88 | "token": { 89 | "type": "tokens", 90 | "id": "db62ca5f-44d4-4cc5-9fed-3891cf4ae2b4", 91 | "created_at": "2021-12-17T13:37:34.508321Z", 92 | "external_id": "0x1a725171cbe7dd52eecb2d1c7b7e23b991608bcdaf53f36b72dcedbc77ce27ff", 93 | "updated_at": "2021-12-17T13:37:34.508321Z", 94 | "contract": { 95 | "type": "contracts", 96 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b" 97 | }, 98 | "metadata": { 99 | "type": "metadata", 100 | "id": "b66ce19f-f742-4e53-9d96-ff63d8c81bab", 101 | "attributes": {}, 102 | "created_at": "2021-12-17T13:37:33.965001Z", 103 | "description": "asdasd", 104 | "image_url": "https://my-fake-url.com/example", 105 | "name": "test", 106 | "updated_at": "2021-12-17T13:37:33.965001Z", 107 | "network": { 108 | "type": "networks", 109 | "id": "testnet" 110 | }, 111 | "owner": { 112 | "type": "identities", 113 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 114 | }, 115 | "relationshipNames": [ 116 | "network", 117 | "owner" 118 | ] 119 | }, 120 | "relationshipNames": [ 121 | "contract", 122 | "metadata" 123 | ] 124 | }, 125 | "relationshipNames": [ 126 | "owner", 127 | "token" 128 | ] 129 | }, 130 | "relationshipNames": [ 131 | "meta_sell_order" 132 | ] 133 | }, 134 | { 135 | "type": "meta-buy-order", 136 | "id": "03980a87-9afb-4952-86c6-674eaf555e51", 137 | "amount": 1, 138 | "claimable_at": "2022-01-25T14:29:17.230663Z", 139 | "created_at": "2022-02-01T08:39:08.900574Z", 140 | "email": "test@test.com", 141 | "state": "receiver_pending", 142 | "verification_url": "", 143 | "meta_sell_order": { 144 | "type": "meta-sell-order", 145 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 146 | "amount": 10, 147 | "available_amount": 2, 148 | "currency": "USD", 149 | "price": 200, 150 | "views_count": 0, 151 | "owner": { 152 | "type": "identities", 153 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 154 | }, 155 | "token": { 156 | "type": "tokens", 157 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 158 | "created_at": "2021-12-29T14:36:02.065904Z", 159 | "external_id": "0x5d40545cdc1cd0cc9961c2561e531e8a9ed0a9b44531b249dcd931bf1db09a", 160 | "updated_at": "2021-12-29T14:36:02.065904Z", 161 | "contract": { 162 | "type": "contracts", 163 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b" 164 | }, 165 | "metadata": { 166 | "type": "metadata", 167 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 168 | "attributes": {}, 169 | "created_at": "2021-12-29T14:36:01.796775Z", 170 | "description": "asd", 171 | "image_url": "https://my-fake-url.com/2b3a17df-932d-4421-b03c-b05cebc7eae", 172 | "name": "response", 173 | "updated_at": "2021-12-29T14:36:01.796775Z", 174 | "network": { 175 | "type": "networks", 176 | "id": "testnet" 177 | }, 178 | "owner": { 179 | "type": "identities", 180 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 181 | }, 182 | "relationshipNames": [ 183 | "network", 184 | "owner" 185 | ] 186 | }, 187 | "relationshipNames": [ 188 | "contract", 189 | "metadata" 190 | ] 191 | }, 192 | "relationshipNames": [ 193 | "owner", 194 | "token" 195 | ] 196 | }, 197 | "relationshipNames": [ 198 | "meta_sell_order" 199 | ] 200 | }, 201 | { 202 | "type": "meta-buy-order", 203 | "id": "497314c0-3d99-4a22-aebb-0c60913324da", 204 | "amount": 1, 205 | "claimable_at": "2022-01-25T14:29:17.230663Z", 206 | "created_at": "2022-01-25T08:31:18.15616Z", 207 | "email": "test@test.net", 208 | "state": "receiver_pending", 209 | "verification_url": "", 210 | "meta_sell_order": { 211 | "type": "meta-sell-order", 212 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 213 | "amount": 10, 214 | "available_amount": 2, 215 | "currency": "USD", 216 | "price": 200, 217 | "views_count": 0, 218 | "owner": { 219 | "type": "identities", 220 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 221 | }, 222 | "token": { 223 | "type": "tokens", 224 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 225 | "created_at": "2021-12-29T14:36:02.065904Z", 226 | "external_id": "0x5d40545cdc1cd0cc9961c2561e531e8a9ed0a9b44531b249dcd931bf1db09a", 227 | "updated_at": "2021-12-29T14:36:02.065904Z", 228 | "contract": { 229 | "type": "contracts", 230 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b" 231 | }, 232 | "metadata": { 233 | "type": "metadata", 234 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 235 | "attributes": {}, 236 | "created_at": "2021-12-29T14:36:01.796775Z", 237 | "description": "asd", 238 | "image_url": "https://my-fake-url.com/2b3a17df-932d-4421-b03c-b05cebc7eae", 239 | "name": "response", 240 | "updated_at": "2021-12-29T14:36:01.796775Z", 241 | "network": { 242 | "type": "networks", 243 | "id": "testnet" 244 | }, 245 | "owner": { 246 | "type": "identities", 247 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 248 | }, 249 | "relationshipNames": [ 250 | "network", 251 | "owner" 252 | ] 253 | }, 254 | "relationshipNames": [ 255 | "contract", 256 | "metadata" 257 | ] 258 | }, 259 | "relationshipNames": [ 260 | "owner", 261 | "token" 262 | ] 263 | }, 264 | "relationshipNames": [ 265 | "meta_sell_order" 266 | ] 267 | }, 268 | { 269 | "type": "meta-buy-order", 270 | "id": "c5b5ca54-1760-43a5-a8ef-b48aad0e30d7", 271 | "amount": 1, 272 | "claimable_at": "2022-01-25T14:29:17.230663Z", 273 | "created_at": "2022-01-25T08:29:07.054527Z", 274 | "email": "test@test.net", 275 | "state": "receiver_pending", 276 | "verification_url": "", 277 | "meta_sell_order": { 278 | "type": "meta-sell-order", 279 | "id": "c83891dd-7034-4b53-8da9-86dac5161fb1", 280 | "amount": 10, 281 | "available_amount": 2, 282 | "currency": "USD", 283 | "price": 200, 284 | "views_count": 0, 285 | "owner": { 286 | "type": "identities", 287 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 288 | }, 289 | "token": { 290 | "type": "tokens", 291 | "id": "7295410b-f0ac-437c-8781-75bf8496ed38", 292 | "created_at": "2021-12-29T14:36:02.065904Z", 293 | "external_id": "0x5d40545cdc1cd0cc9961c2561e531e8a9ed0a9b44531b249dcd931bf1db09a", 294 | "updated_at": "2021-12-29T14:36:02.065904Z", 295 | "contract": { 296 | "type": "contracts", 297 | "id": "6b1658e4-d3cf-426d-a10d-8edad1055c3b" 298 | }, 299 | "metadata": { 300 | "type": "metadata", 301 | "id": "c4b6da1f-6ca1-43ce-bea8-515cdf29cd59", 302 | "attributes": {}, 303 | "created_at": "2021-12-29T14:36:01.796775Z", 304 | "description": "asd", 305 | "image_url": "https://my-fake-url.com/2b3a17df-932d-4421-b03c-b05cebc7eae", 306 | "name": "response", 307 | "updated_at": "2021-12-29T14:36:01.796775Z", 308 | "network": { 309 | "type": "networks", 310 | "id": "testnet" 311 | }, 312 | "owner": { 313 | "type": "identities", 314 | "id": "3d38fff3-847f-45f2-a267-891ba90dac37" 315 | }, 316 | "relationshipNames": [ 317 | "network", 318 | "owner" 319 | ] 320 | }, 321 | "relationshipNames": [ 322 | "contract", 323 | "metadata" 324 | ] 325 | }, 326 | "relationshipNames": [ 327 | "owner", 328 | "token" 329 | ] 330 | }, 331 | "relationshipNames": [ 332 | "meta_sell_order" 333 | ] 334 | } 335 | ] 336 | --------------------------------------------------------------------------------