├── .prettierignore ├── .gitignore ├── src ├── example │ └── .gitignore ├── lib │ ├── constants.ts │ ├── helpers │ │ ├── index.ts │ │ └── polyfills.ts │ ├── service │ │ └── http.ts │ ├── types │ │ ├── image.ts │ │ ├── crud.ts │ │ ├── auth.ts │ │ └── base.ts │ ├── strapi-client.ts │ ├── strapi-auth-client.ts │ ├── strapi-client-helper.ts │ ├── strapi-query-builder.ts │ └── strapi-filter-builder.ts └── index.ts ├── .eslintignore ├── .prettierrc ├── nodemon.json ├── .eslintrc ├── .github └── workflows │ └── publish.yml ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md └── tsconfig.json /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /src/example/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | src/example -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "trailingComma": "es5", 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY = 'strapi.auth.token'; 2 | 3 | const AuthUrl = { 4 | signIn: '/auth/local', 5 | signUp: '/auth/local/register', 6 | getMe: '/users/me', 7 | }; 8 | 9 | export const EndPoint = { 10 | auth: AuthUrl, 11 | }; 12 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [".git", "node_modules/", "dist/", "coverage/"], 4 | "watch": ["src/"], 5 | "execMap": { 6 | "ts": "node -r ts-node/register" 7 | }, 8 | "env": { 9 | "NODE_ENV": "development" 10 | }, 11 | "ext": "js,json,ts" 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "rules": { 12 | "no-console": "warn", 13 | "prettier/prettier": 2, 14 | "@typescript-eslint/no-explicit-any": ["off"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { stringify, parse } from 'qs'; 2 | 3 | export function generateQueryString(obj: object): string { 4 | return stringify(obj, { encodeValuesOnly: true }); 5 | } 6 | 7 | export function generateQueryFromRawString(rawQuery: string): string { 8 | return stringify(parse(rawQuery), { encodeValuesOnly: true }); 9 | } 10 | 11 | export const isBrowser = () => typeof window !== 'undefined'; 12 | 13 | export const stringToArray = (value: string): string[] => { 14 | return value.split('.'); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/service/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; 2 | 3 | export const getAxiosInstance = (url: string, apiToken?: string): AxiosInstance => { 4 | const API = axios.create(); 5 | 6 | API.defaults.baseURL = url; 7 | 8 | const axiosConfig = (config: AxiosRequestConfig): AxiosRequestConfig => { 9 | if (apiToken) { 10 | config.headers = { 11 | Authorization: `Bearer ${apiToken}`, 12 | }; 13 | } 14 | return config; 15 | }; 16 | 17 | API.interceptors.request.use(axiosConfig); 18 | 19 | return API; 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/helpers/polyfills.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | // @ts-nocheck 3 | 4 | /** 5 | * https://mathiasbynens.be/notes/globalthis 6 | */ 7 | export function polyfillGlobalThis() { 8 | if (typeof globalThis === 'object') return; 9 | try { 10 | Object.defineProperty(Object.prototype, '__magic__', { 11 | get: function () { 12 | return this; 13 | }, 14 | configurable: true, 15 | }); 16 | __magic__.globalThis = __magic__; 17 | delete Object.prototype.__magic__; 18 | } catch (e) { 19 | if (typeof self !== 'undefined') { 20 | self.globalThis = self; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Release CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Setup Node 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '14.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - name: Install dependencies and build 🔧 21 | run: npm install && npm run build 22 | - name: Publish package on NPM 📦 23 | run: npm run semantic-release 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable no-undef */ 3 | import dts from 'rollup-plugin-dts'; 4 | import esbuild from 'rollup-plugin-esbuild'; 5 | 6 | const name = require('./package.json').main.replace(/\.js$/, ''); 7 | 8 | const bundle = config => ({ 9 | ...config, 10 | input: 'src/index.ts', 11 | external: id => !/^[./]/.test(id), 12 | }); 13 | 14 | export default [ 15 | bundle({ 16 | plugins: [esbuild()], 17 | output: [ 18 | { 19 | file: `${name}.js`, 20 | format: 'cjs', 21 | sourcemap: true, 22 | }, 23 | { 24 | file: `${name}.mjs`, 25 | format: 'es', 26 | sourcemap: true, 27 | }, 28 | ], 29 | }), 30 | bundle({ 31 | plugins: [dts()], 32 | output: { 33 | file: `${name}.d.ts`, 34 | format: 'es', 35 | }, 36 | }), 37 | ]; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Karthikeyan Mariappan 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. -------------------------------------------------------------------------------- /src/lib/types/image.ts: -------------------------------------------------------------------------------- 1 | import { StrapiUnifiedResponse } from './base'; 2 | 3 | export type ImageUnifiedResponse = StrapiUnifiedResponse; 4 | 5 | export type StrapiImage = { 6 | name: string; 7 | alternativeText: string; 8 | caption: string; 9 | width: number; 10 | height: number; 11 | formats: ImageFormats; 12 | hash: string; 13 | ext: string; 14 | mime: string; 15 | size: number; 16 | url: string; 17 | previewUrl: string | null; 18 | provider: string; 19 | provider_metadata: ImageProviderMetaData; 20 | createdAt: string; 21 | updatedAt: string; 22 | }; 23 | 24 | type ImageFormatAttribute = { 25 | ext: string; 26 | url: string; 27 | hash: string; 28 | mime: string; 29 | name: string; 30 | path: string | null; 31 | size: number; 32 | width: number; 33 | height: number; 34 | provider_metadata: ImageProviderMetaData; 35 | }; 36 | 37 | type ImageProviderMetaData = { 38 | public_id: string; 39 | resource_type: string; 40 | }; 41 | 42 | type ImageFormats = { 43 | large?: ImageFormatAttribute; 44 | small?: ImageFormatAttribute; 45 | medium?: ImageFormatAttribute; 46 | thumbnail?: ImageFormatAttribute; 47 | }; 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { StrapiClient } from './lib/strapi-client'; 2 | import { StrapiClientOptions } from './lib/types/base'; 3 | 4 | const defaultOptions: StrapiClientOptions = { 5 | url: '', 6 | normalizeData: true, 7 | }; 8 | 9 | /** 10 | * Strapi Client Options Object 11 | * 12 | * @param url Strapi application url 13 | * 14 | * @param apiToken Authorized Api Token 15 | * 16 | * @param normalizeData Disables Unified response format. default - true 17 | * 18 | * @param headers custom headers 19 | * 20 | * @param debug Query log on development. default - false 21 | * 22 | * @param persistSession Using browser localstorage to save the current session. default- flase 23 | * 24 | */ 25 | const createClient = (options: StrapiClientOptions): StrapiClient => { 26 | return new StrapiClient({ ...defaultOptions, ...options }); 27 | }; 28 | 29 | export { createClient, StrapiClient }; 30 | export type { 31 | StrapiUnifiedResponse, 32 | StrapiTimestamp, 33 | StrapiPopulatedResponse, 34 | StrapiClientOptions, 35 | } from './lib/types/base'; 36 | export type { SignInCredentials, SignUpCredentials } from './lib/types/auth'; 37 | export type { StrapiImage } from './lib/types/image'; 38 | -------------------------------------------------------------------------------- /src/lib/types/crud.ts: -------------------------------------------------------------------------------- 1 | export type CrudOperators = 2 | | 'eq' 3 | | 'ne' 4 | | 'lt' 5 | | 'gt' 6 | | 'lte' 7 | | 'gte' 8 | | 'in' 9 | | 'notIn' 10 | | 'contains' 11 | | 'notContains' 12 | | 'containsi' 13 | | 'notContainsi' 14 | | 'between' 15 | | 'null' 16 | | 'notNull' 17 | | 'startsWith' 18 | | 'endsWith'; 19 | 20 | export type RelationalFilterOperators = 21 | | 'eq' 22 | | 'ne' 23 | | 'lt' 24 | | 'gt' 25 | | 'lte' 26 | | 'gte' 27 | | 'in' 28 | | 'notIn' 29 | | 'contains' 30 | | 'notContains' 31 | | 'startsWith' 32 | | 'endsWith'; 33 | 34 | export type CrudFilter = { 35 | field: keyof T; 36 | operator: CrudOperators; 37 | value: string | number | Array; 38 | }; 39 | export type CrudSort = { 40 | field: keyof T; 41 | order?: 'asc' | 'desc'; 42 | }; 43 | 44 | export type DeepFilterType = { 45 | path: Array; 46 | operator: RelationalFilterOperators; 47 | value: string | number | Array; 48 | }; 49 | 50 | type DeepChild = { 51 | key: string; 52 | fields?: string[]; 53 | }; 54 | 55 | export type PopulateDeepOptions = { 56 | path: string; 57 | fields?: string[]; 58 | children?: DeepChild[] | '*'; 59 | }; 60 | 61 | export declare type CrudSorting = CrudSort[]; 62 | 63 | export type CrudFilters = Array>; 64 | -------------------------------------------------------------------------------- /src/lib/types/auth.ts: -------------------------------------------------------------------------------- 1 | import { StrapiApiError } from './base'; 2 | 3 | export type AuthenticationResponse = { 4 | data: AuthData | null; 5 | error?: StrapiApiError; 6 | }; 7 | 8 | export type SignInCredentials = { 9 | email: string; 10 | password: string; 11 | }; 12 | 13 | export type SignUpCredentials = { 14 | username: string; 15 | email: string; 16 | password: string; 17 | }; 18 | 19 | export type Provider = 20 | | 'auth0' 21 | | 'cas' 22 | | 'cognito' 23 | | 'discord' 24 | | 'email' 25 | | 'facebook' 26 | | 'github' 27 | | 'google' 28 | | 'instagram' 29 | | 'linkedin' 30 | | 'microsoft' 31 | | 'reddit' 32 | | 'twitch' 33 | | 'twitter' 34 | | 'vk'; 35 | 36 | export type User = { 37 | id: number; 38 | username: string; 39 | email: string; 40 | provider: string; 41 | confirmed: boolean; 42 | blocked: boolean; 43 | createdAt: string; 44 | updatedAt: string; 45 | }; 46 | 47 | export type AuthData = { 48 | jwt: 'string'; 49 | user: User | null; 50 | provider?: Provider; 51 | }; 52 | 53 | export interface Session { 54 | access_token: string; 55 | user: User | null; 56 | /** 57 | * The number of seconds until the token expires (since it was issued). Returned when a login is confirmed. 58 | */ 59 | expires_in?: number; 60 | /** 61 | * A timestamp of when the token will expire. Returned when a login is confirmed. 62 | */ 63 | expires_at?: number; 64 | refresh_token?: string; 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/types/base.ts: -------------------------------------------------------------------------------- 1 | export interface StrapiApiError { 2 | message: string | null; 3 | status: number | null; 4 | name: string | null; 5 | details: any | null; 6 | } 7 | 8 | export type StrapiClientOptions = { 9 | url: string; 10 | debug?: boolean; 11 | normalizeData?: boolean; 12 | apiToken?: string; 13 | headers?: { [key: string]: string }; 14 | persistSession?: boolean; 15 | localStorage?: SupportedStorage; 16 | }; 17 | 18 | type StrapiPagination = { 19 | page: number; 20 | pageSize: number; 21 | pageCount: number; 22 | total: number; 23 | }; 24 | 25 | export type Meta = { 26 | pagination: StrapiPagination; 27 | }; 28 | 29 | export type StrapiApiResponse = { 30 | data: T | null; 31 | error?: StrapiApiError; 32 | meta?: Meta; 33 | }; 34 | 35 | export enum PublicationState { 36 | LIVE = 'live', 37 | PREVIEW = 'preview', 38 | } 39 | 40 | export type StrapiUnifiedResponse = { 41 | id: number | string; 42 | attributes: T; 43 | }; 44 | 45 | export type InferedTypeFromArray = T extends Array ? U : T; 46 | 47 | export type StrapiPopulatedResponse = { 48 | data: T extends Array ? Array> : StrapiUnifiedResponse; 49 | }; 50 | 51 | export type StrapiTimestamp = { 52 | createdAt: string; 53 | updatedAt: string; 54 | publishedAt: string; 55 | }; 56 | 57 | export type StrapiLocalization = { 58 | locale: string; 59 | localizations?: Array; 60 | }; 61 | 62 | // Persist Token in LocalStorage 63 | type AnyFunction = (...args: any[]) => any; 64 | type MaybePromisify = T | Promise; 65 | 66 | type PromisifyMethods = { 67 | [K in keyof T]: T[K] extends AnyFunction ? (...args: Parameters) => MaybePromisify> : T[K]; 68 | }; 69 | 70 | export type SupportedStorage = PromisifyMethods>; 71 | -------------------------------------------------------------------------------- /src/lib/strapi-client.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { StrapiAuthClient } from './strapi-auth-client'; 3 | import { getAxiosInstance } from './service/http'; 4 | import { StrapiClientOptions } from './types/base'; 5 | import { StrapiQueryBuilder } from './strapi-query-builder'; 6 | 7 | export class StrapiClient { 8 | private httpClient: AxiosInstance; 9 | private options: StrapiClientOptions; 10 | private isNotUserContent: boolean; 11 | private normalizeData: boolean; 12 | private debug: boolean; 13 | 14 | constructor(options: StrapiClientOptions) { 15 | this.debug = options.debug || false; 16 | this.httpClient = getAxiosInstance(options.url, options.apiToken); 17 | this.auth = this._initStrapiAuthClient(this.httpClient); 18 | this.normalizeData = options.normalizeData ? options.normalizeData : false; 19 | this.options = options; 20 | this.isNotUserContent = true; 21 | } 22 | 23 | auth: StrapiAuthClient; 24 | 25 | /** 26 | * Perform a model operation. 27 | * 28 | * @param name The model name to operate on. 29 | */ 30 | from(contentName: string): StrapiQueryBuilder { 31 | contentName === 'users' ? (this.isNotUserContent = false) : (this.isNotUserContent = true); 32 | const url = `${this.options.url}/${contentName}`; 33 | return new StrapiQueryBuilder(url, this.httpClient, this.isNotUserContent, this.normalizeData, this.debug); 34 | } 35 | 36 | /** 37 | * 38 | * @returns The registered Api URL 39 | */ 40 | getApiUrl(): string { 41 | return this.options.url; 42 | } 43 | 44 | setToken(token: string): void { 45 | this.httpClient.defaults.headers.common['Authorization'] = `Bearer ${token}`; 46 | } 47 | 48 | removeToken(): void { 49 | delete this.httpClient.defaults.headers.common['Authorization']; 50 | } 51 | 52 | private _initStrapiAuthClient(axiosInstance: AxiosInstance) { 53 | return new StrapiAuthClient(axiosInstance, this.options); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kmariappan/strapi-client-js", 3 | "author": "Karthikeyan Mariappan", 4 | "description": "Javascript client for Strapi Rest API", 5 | "version": "0.0.0-development", 6 | "main": "dist/strapi-client.js", 7 | "module": "dist/strapi-client.mjs", 8 | "typings": "dist/strapi-client.d.ts", 9 | "license": "MIT", 10 | "engines": { 11 | "node": ">=14" 12 | }, 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "scripts": { 21 | "build": "rollup -c", 22 | "dev": "nodemon --config nodemon.json src/example/play.ts", 23 | "lint": "eslint . --ext .ts", 24 | "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", 25 | "semantic-release": "semantic-release --branches main", 26 | "commit": "git-cz" 27 | }, 28 | "husky": { 29 | "hooks": { 30 | "pre-commit": "npm run prettier-format && npm run lint" 31 | } 32 | }, 33 | "peerDependencies": { 34 | "axios": "^0.26.0", 35 | "qs": "^6.10.3" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^16.9.4", 39 | "@types/qs": "^6.9.7", 40 | "@typescript-eslint/eslint-plugin": "^5.12.1", 41 | "@typescript-eslint/parser": "^5.12.1", 42 | "ansi-regex": "^6.0.1", 43 | "axios": "^0.26.0", 44 | "cz-conventional-changelog": "^3.3.0", 45 | "esbuild": "^0.14.23", 46 | "eslint": "^8.9.0", 47 | "eslint-config-prettier": "^8.4.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "husky": "^7.0.4", 50 | "nodemon": "^2.0.12", 51 | "prettier": "^2.5.1", 52 | "qs": "^6.10.3", 53 | "rimraf": "^3.0.2", 54 | "rollup": "^2.68.0", 55 | "rollup-plugin-dts": "^4.1.0", 56 | "rollup-plugin-esbuild": "^4.8.2", 57 | "semantic-release": "^19.0.2", 58 | "ts-node": "^10.2.1", 59 | "typescript": "^4.4.3" 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "./node_modules/cz-conventional-changelog" 64 | } 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/kmariappan/strapi-client-js.git" 69 | }, 70 | "dependencies": {} 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/strapi-auth-client.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { StrapiApiError, StrapiApiResponse, StrapiClientOptions, SupportedStorage } from './types/base'; 3 | 4 | import { AuthData, Session, SignInCredentials, SignUpCredentials, User } from './types/auth'; 5 | 6 | import { EndPoint, STORAGE_KEY } from './constants'; 7 | import { polyfillGlobalThis } from './helpers/polyfills'; 8 | import { isBrowser } from './helpers'; 9 | import { StrapiClientHelper } from './strapi-client-helper'; 10 | 11 | polyfillGlobalThis(); // Make "globalThis" available 12 | 13 | const DEFAULT_OPTIONS = { 14 | autoRefreshToken: true, 15 | persistSession: true, 16 | detectSessionInUrl: true, 17 | }; 18 | 19 | export class StrapiAuthClient extends StrapiClientHelper { 20 | private httpClient: AxiosInstance; 21 | 22 | protected localStorage: SupportedStorage; 23 | protected autoRefreshToken: boolean; 24 | protected persistSession: boolean; 25 | 26 | /** 27 | * The currently logged in user or null. 28 | */ 29 | protected currentUser: User | null; 30 | /** 31 | * The session object for the currently logged in user or null. 32 | */ 33 | protected currentSession: Session | null; 34 | 35 | constructor(axiosInstance: AxiosInstance, options: StrapiClientOptions) { 36 | const settings = { ...DEFAULT_OPTIONS, ...options }; 37 | super(settings.url); 38 | this.httpClient = axiosInstance; 39 | this.currentUser = null; 40 | this.currentSession = null; 41 | this.autoRefreshToken = settings.autoRefreshToken; 42 | this.persistSession = settings.persistSession; 43 | this.localStorage = settings.localStorage || globalThis.localStorage; 44 | } 45 | 46 | /** 47 | * 48 | * @param credentials email and password 49 | * @returns data and error objects, data object contains jwt, user and provider 50 | */ 51 | public signIn(credentials: SignInCredentials): Promise> { 52 | return new Promise>((resolve) => { 53 | this.httpClient 54 | .post(EndPoint.auth.signIn, { 55 | identifier: credentials.email, 56 | password: credentials.password, 57 | }) 58 | .then((res) => { 59 | this._saveSession({ 60 | access_token: res.data.jwt, 61 | user: res.data.user, 62 | }); 63 | resolve({ 64 | data: res.data, 65 | }); 66 | }) 67 | .catch((err) => { 68 | if (err) { 69 | return resolve(this._returnErrorHandler(err)); 70 | } 71 | }); 72 | }); 73 | } 74 | 75 | /** 76 | * 77 | * @param credentials object contains username, email and password 78 | * @returns data and error objects, data object contains jwt, user and provider 79 | */ 80 | 81 | public async signUp(credentials: SignUpCredentials): Promise> { 82 | return new Promise>((resolve) => { 83 | this.httpClient 84 | .post(EndPoint.auth.signUp, credentials) 85 | .then((res) => { 86 | resolve({ data: res.data }); 87 | this._saveSession({ 88 | access_token: res.data.jwt, 89 | user: res.data.user, 90 | }); 91 | }) 92 | .catch((err) => { 93 | if (err) { 94 | if (err) { 95 | return resolve(this._returnErrorHandler(err)); 96 | } 97 | } 98 | }); 99 | }); 100 | } 101 | 102 | /** 103 | * 104 | * @returns Get the user object by JWT token 105 | */ 106 | public async getMe(): Promise> { 107 | return new Promise>((resolve) => { 108 | this.httpClient 109 | .get(EndPoint.auth.getMe) 110 | .then((res) => { 111 | resolve({ data: res.data }); 112 | }) 113 | .catch((err: any) => { 114 | if (err) { 115 | const error = err.response.data.error as StrapiApiError; 116 | return resolve({ 117 | data: null, 118 | error, 119 | }); 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | /** 126 | * Inside a browser context, `signOut()` will remove the logged in user from the browser session 127 | * and log them out - removing all items from localstorage and then trigger a "SIGNED_OUT" event. 128 | * 129 | * For server-side management, you can disable sessions by passing a JWT through to `auth.api.signOut(JWT: string)` 130 | */ 131 | async signOut(): Promise<{ error: StrapiApiError | null }> { 132 | const accessToken = this.currentSession?.access_token; 133 | this._removeSession(); 134 | if (accessToken) { 135 | // const { error } = await this.api.signOut(accessToken); 136 | // if (error) return { error }; 137 | } 138 | return { error: null }; 139 | } 140 | 141 | /** 142 | * set currentSession and currentUser 143 | * process to _startAutoRefreshToken if possible 144 | */ 145 | private _saveSession(session: Session) { 146 | this.currentSession = session; 147 | this.currentUser = session.user; 148 | if (this.persistSession) { 149 | this._persistSession(this.currentSession); 150 | } 151 | } 152 | 153 | private async _removeSession() { 154 | this.currentSession = null; 155 | this.currentUser = null; 156 | // if (this.refreshTokenTimer) clearTimeout(this.refreshTokenTimer) 157 | isBrowser() && (await this.localStorage.removeItem(STORAGE_KEY)); 158 | } 159 | 160 | private _persistSession(currentSession: Session) { 161 | const data = { currentSession, expiresAt: currentSession.expires_at }; 162 | isBrowser() && this.localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/lib/strapi-client-helper.ts: -------------------------------------------------------------------------------- 1 | import { InferedTypeFromArray, StrapiApiError, StrapiApiResponse } from './types/base'; 2 | import { CrudFilter, CrudSorting, DeepFilterType, PopulateDeepOptions } from './types/crud'; 3 | import { parse, stringify } from 'qs'; 4 | import { generateQueryString, stringToArray } from './helpers'; 5 | 6 | export abstract class StrapiClientHelper { 7 | protected url: string; 8 | 9 | constructor(url: string) { 10 | this.url = url; 11 | } 12 | 13 | private _normalizeData(data: any): any { 14 | const isObject = (data: any) => Object.prototype.toString.call(data) === '[object Object]'; 15 | 16 | const flatten = (data: any) => { 17 | if (!data.attributes) return data; 18 | 19 | return { 20 | id: data.id, 21 | ...data.attributes, 22 | }; 23 | }; 24 | 25 | if (Array.isArray(data)) { 26 | return data.map((item) => this._normalizeData(item)); 27 | } 28 | 29 | if (isObject(data)) { 30 | if (Array.isArray(data.data)) { 31 | data = [...data.data]; 32 | } else if (isObject(data.data)) { 33 | data = flatten({ ...data.data }); 34 | } else if (data.data === null) { 35 | data = null; 36 | } else { 37 | data = flatten(data); 38 | } 39 | 40 | for (const key in data) { 41 | data[key] = this._normalizeData(data[key]); 42 | } 43 | 44 | return data; 45 | } 46 | 47 | return data; 48 | } 49 | 50 | protected _returnDataHandler(data: StrapiApiResponse): StrapiApiResponse { 51 | const response: StrapiApiResponse = { 52 | data: this._normalizeData(data.data) as T, 53 | meta: data.meta, 54 | error: data.error, 55 | }; 56 | return response; 57 | } 58 | 59 | protected _returnErrorHandler(err: any): StrapiApiResponse { 60 | let error: StrapiApiError = { 61 | status: null, 62 | message: null, 63 | details: null, 64 | name: null, 65 | }; 66 | 67 | if (err.code === 'ENOTFOUND' || err.syscall === 'getaddrinfo') { 68 | error.status = err.code; 69 | error.message = `The given url ${err.config.baseURL} is incorrect or invalid `; 70 | error.name = err.syscall; 71 | } else { 72 | if (!err.response.data.error) { 73 | error.status = err.response.status as number; 74 | error.message = err.response.statusText; 75 | error.name = err.response.data; 76 | } else { 77 | error = err.response.data.error as StrapiApiError; 78 | } 79 | } 80 | 81 | const response: StrapiApiResponse = { 82 | data: null, 83 | error, 84 | }; 85 | return response; 86 | } 87 | 88 | protected _generateFilter({ field, operator, value }: CrudFilter>): string { 89 | let rawQuery = ''; 90 | if (Array.isArray(value)) { 91 | value.map((val) => { 92 | rawQuery += `&filters[${field}][$${operator}]=${val}`; 93 | }); 94 | } else { 95 | rawQuery += `&filters[${field}][$${operator}]=${value}`; 96 | } 97 | const parsedQuery = parse(rawQuery); 98 | return this._handleUrl(generateQueryString(parsedQuery)); 99 | } 100 | 101 | protected _genrateRelationsFilter(deepFilter: DeepFilterType) { 102 | let rawQuery = `filters`; 103 | const { path: fields, operator, value } = deepFilter; 104 | if (Array.isArray(fields)) { 105 | fields.map((field) => { 106 | rawQuery += `[${field}]`; 107 | }); 108 | } 109 | 110 | const partialQuery = rawQuery; 111 | 112 | if (Array.isArray(value)) { 113 | value.map((val, index) => { 114 | if (index === 0) { 115 | rawQuery += `[$${operator}]=${val}`; 116 | } else { 117 | rawQuery += `&${partialQuery}[$${operator}]=${val}`; 118 | } 119 | }); 120 | } else { 121 | rawQuery += `[$${operator}]=${value}`; 122 | } 123 | 124 | const parsedQuery = parse(rawQuery); 125 | return this._handleUrl(generateQueryString(parsedQuery)); 126 | } 127 | 128 | protected _generateSort(_sort: CrudSorting): string { 129 | const sort: string[] = []; 130 | _sort.map((item) => { 131 | if (item.order) { 132 | sort.push(`${item.field}:${item.order}`); 133 | } else { 134 | sort.push(`${item.field}`); 135 | } 136 | }); 137 | return this._handleUrl(generateQueryString({ sort })); 138 | } 139 | 140 | protected _handleUrl(query: string): string { 141 | const lastChar = this.url.charAt(this.url.length - 1); 142 | const hasQuerySymbol = this.url.includes('?'); 143 | if (!hasQuerySymbol && lastChar !== '&') { 144 | return `${this.url}?${query}`; 145 | } else { 146 | return `${this.url}&${query}`; 147 | } 148 | } 149 | 150 | protected _generatePopulateDeep(options: PopulateDeepOptions[]) { 151 | let url_string = ''; 152 | options.map((q) => { 153 | const manipulatedPath = stringToArray(q.path); 154 | let partialQuery = ''; 155 | if (Array.isArray(manipulatedPath)) { 156 | manipulatedPath.map((path, i) => { 157 | partialQuery += i === 0 ? `populate[${path}]` : `[populate][${path}]`; 158 | }); 159 | } 160 | 161 | if (q.fields) { 162 | q.fields.map((field, i) => { 163 | url_string += 164 | i === 0 && url_string === '' 165 | ? `${partialQuery}[fields][${i}]=${field}` 166 | : `&${partialQuery}[fields][${i}]=${field}`; 167 | }); 168 | } 169 | 170 | if (q.children === '*') { 171 | url_string += `&${partialQuery}[populate]=%2A`; 172 | } 173 | 174 | if (q.children && q.children !== '*') { 175 | const partialQuery2 = partialQuery; 176 | let someQuery = ''; 177 | q.children.map((child) => { 178 | if (!child.fields) { 179 | url_string += `&${partialQuery2}[populate][${child.key}]=%2A`; 180 | } else { 181 | child.fields.map((field, ind) => { 182 | someQuery += `&${partialQuery2}[populate][${child.key}][fields][${ind}]=${field}`; 183 | }); 184 | } 185 | 186 | url_string += `${someQuery}`; 187 | }); 188 | } 189 | }); 190 | 191 | return this._handleUrl(stringify(parse(url_string))); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/lib/strapi-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { generateQueryString } from './helpers'; 3 | import { StrapiClientHelper } from './strapi-client-helper'; 4 | import { StrapiFilterBuilder } from './strapi-filter-builder'; 5 | import { StrapiApiResponse } from './types/base'; 6 | 7 | type PostValuesType = { 8 | data: T; 9 | }; 10 | export class StrapiQueryBuilder extends StrapiClientHelper { 11 | private httpClient: AxiosInstance; 12 | private isNotUserContent: boolean; 13 | protected normalizData: boolean; 14 | private debug: boolean; 15 | constructor( 16 | url: string, 17 | axiosInstance: AxiosInstance, 18 | isNotUserContent: boolean, 19 | normalizeData: boolean, 20 | debug: boolean 21 | ) { 22 | super(url); 23 | this.debug = debug; 24 | this.normalizData = normalizeData; 25 | this.url = `${url}`; 26 | this.isNotUserContent = isNotUserContent; 27 | this.httpClient = axiosInstance; 28 | } 29 | 30 | /** 31 | * 32 | * @param fields Array of string to select the fields. 33 | * @returns collection of requested contents. 34 | */ 35 | 36 | select(fields?: Array): StrapiFilterBuilder { 37 | if (fields) { 38 | const query = { 39 | fields, 40 | }; 41 | const queryString = generateQueryString(query); 42 | this.url = `${this.url}?${queryString}`; 43 | } 44 | 45 | return new StrapiFilterBuilder( 46 | this.url, 47 | this.httpClient, 48 | this.normalizData, 49 | this.debug, 50 | this.isNotUserContent 51 | ); 52 | } 53 | 54 | /** 55 | * 56 | * @param ids Array of string or number values to select many records. 57 | * @returns selected contents. 58 | */ 59 | selectManyByID(ids: string[] | number[]): StrapiFilterBuilder { 60 | if (ids) { 61 | const query = ids?.map((item: string | number) => `filters[id][$in]=${item}`).join('&'); 62 | 63 | this.url = `${this.url}?${query}`; 64 | } 65 | 66 | return new StrapiFilterBuilder( 67 | this.url, 68 | this.httpClient, 69 | this.normalizData, 70 | this.debug, 71 | this.isNotUserContent 72 | ); 73 | } 74 | 75 | /** 76 | * 77 | * @param values The values to create a new record. 78 | * @returns By default the new record is returned. 79 | */ 80 | async create(values: T): Promise> { 81 | return new Promise>((resolve) => { 82 | this.httpClient 83 | .post>(this.url, this._handleValues(values)) 84 | .then((res) => { 85 | resolve(this.normalizData ? this._returnDataHandler(res.data) : res.data); 86 | }) 87 | .catch((err) => { 88 | if (err) { 89 | resolve(this._returnErrorHandler(err)); 90 | } 91 | }); 92 | }); 93 | } 94 | 95 | /** 96 | * 97 | * @param values objects of values to create many records. 98 | * @returns return boolean value if the process on success 99 | */ 100 | async createMany(values: T[]): Promise<{ success: true }> { 101 | await Promise.all( 102 | values.map(async (value): Promise> => { 103 | const { data } = await this.httpClient.post>(this.url, this._handleValues(value)); 104 | return Promise.resolve(data); 105 | }) 106 | ).catch((error) => { 107 | if (error) { 108 | this._returnErrorHandler(error); 109 | } 110 | }); 111 | return Promise.resolve({ 112 | success: true, 113 | }); 114 | } 115 | 116 | /** 117 | * 118 | * @param values The values to update an existing record. 119 | * @returns By default the new record is returned. 120 | */ 121 | async update(id: string | number, values: Partial): Promise> { 122 | const url = `${this.url}/${id}`; 123 | return new Promise>((resolve) => { 124 | this.httpClient 125 | .put>(url, this._handleValues(values)) 126 | .then((res) => { 127 | resolve(this.normalizData ? this._returnDataHandler(res.data) : res.data); 128 | }) 129 | .catch((err) => { 130 | if (err) { 131 | resolve(this._returnErrorHandler(err)); 132 | } 133 | }); 134 | }); 135 | } 136 | 137 | /** 138 | * 139 | * @param values objects of values to update many records. 140 | * @returns return boolean value if the process on success 141 | */ 142 | async updateMany(values: { id: string | number; variables: Partial }[]): Promise<{ success: true }> { 143 | await Promise.all( 144 | values.map(async (value): Promise> => { 145 | const url = `${this.url}/${value.id}`; 146 | 147 | const { data } = await this.httpClient.put>(url, this._handleValues(value.variables)); 148 | return Promise.resolve(data); 149 | }) 150 | ).catch((error) => { 151 | if (error) { 152 | this._returnErrorHandler(error); 153 | } 154 | }); 155 | return Promise.resolve({ 156 | success: true, 157 | }); 158 | } 159 | 160 | /** 161 | * 162 | * @param value The value to delete an record. 163 | * @returns By default the deleted record is returned. 164 | */ 165 | async deleteOne(id: string | number): Promise> { 166 | const url = `${this.url}/${id}`; 167 | return new Promise>((resolve) => { 168 | this.httpClient 169 | .delete>(url) 170 | .then((res) => { 171 | resolve(res.data); 172 | }) 173 | .catch((err) => { 174 | if (err) { 175 | resolve(this._returnErrorHandler(err)); 176 | } 177 | }); 178 | }); 179 | } 180 | 181 | /** 182 | * 183 | * @param values Array of string or number values to delete many records. 184 | * @returns return boolean value if the process on success 185 | */ 186 | async deleteMany(ids: string[] | number[]): Promise<{ success: true }> { 187 | await Promise.all( 188 | ids.map(async (id) => { 189 | const { data } = await this.httpClient.delete(`${this.url}/${id}`); 190 | return data; 191 | }) 192 | ).catch((err) => { 193 | if (err) { 194 | return this._returnErrorHandler(err); 195 | } 196 | }); 197 | 198 | return Promise.resolve({ 199 | success: true, 200 | }); 201 | } 202 | 203 | private _handleValues(values: Partial): Partial | PostValuesType> { 204 | if (this.isNotUserContent) { 205 | const dataValues: PostValuesType> = { 206 | data: values, 207 | }; 208 | return dataValues; 209 | } else { 210 | return values; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strapi-client-js 2 | 3 | Javascript client for Strapi Rest API. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm i @kmariappan/strapi-client-js 9 | 10 | npm i axios qs 11 | 12 | or 13 | 14 | yarn add @kmariappan/strapi-client-js 15 | 16 | yarn add axios qs 17 | 18 | ``` 19 | 20 | ## Peer Dependencies 21 | 22 | This package uses [axios](https://axios-http.com/) as a http client and [qs](https://github.com/ljharb/qs) for parsing and stringifying the Query. 23 | 24 | ### Create Client Options 25 | 26 | ```ts 27 | // Typescript 28 | 29 | import { createClient, StrapiClientOptions } from '@kmariappan/strapi-client-js'; 30 | 31 | const options: StrapiClientOptions = { 32 | url: 'http://localhost:1337/api', 33 | apiToken: '', // Built in API token, 34 | normalizeData: true, // Normalize Unified response Format. default - true 35 | headers: {}, // Custom Headers 36 | persistSession: false, // Persist authenticated token in browser local storage. default -false 37 | }; 38 | 39 | const strapiClient = createClient(options); 40 | ``` 41 | 42 | # REST API 43 | 44 | ## Get 45 | 46 | ```js 47 | import { createClient } from '@kmariappan/strapi-client-js'; 48 | 49 | const strapiClient = createClient({ url: 'http://localhost:1337/api' }); 50 | 51 | const run = async () => { 52 | const { data, error, meta } = await strapiClient 53 | .from('students') 54 | // .from("students") ** typescript ** 55 | .select(['firstname', 'lastname']) // Select only specific fields. 56 | // .select(["firstname", "lastname"]) ** typescript ** 57 | .get(); 58 | 59 | if (error) { 60 | console.log(error); 61 | } else { 62 | console.log(data); 63 | console.log(meta); 64 | } 65 | }; 66 | 67 | run(); 68 | ``` 69 | 70 | Retrieve many entries by ids 71 | 72 | ```ts 73 | const { data, error, meta } = await strapiClient.from('students').selectManyByID([200, 240]).get(); 74 | ``` 75 | 76 | ### Filter Methods 77 | 78 | ```ts 79 | const { data, error, meta } = await strapiClient 80 | .from('students') 81 | .select(['firstname', 'lastname']) 82 | .equalTo('id', 2) 83 | .get(); 84 | ``` 85 | 86 | ```ts 87 | const { data, error, meta } = await strapiClient 88 | .from('students') 89 | .select(['firstname', 'lastname']) 90 | .between('id', [40, 45]) 91 | .get(); 92 | ``` 93 | 94 | ### All filter Methods 95 | 96 | ```js 97 | equalTo(); 98 | notEqualTo(); 99 | lessThan(); 100 | lessThanOrEqualTo(); 101 | greaterThan(); 102 | greaterThanOrEqualTo(); 103 | containsCaseSensitive(); 104 | notContainsCaseSensitive(); 105 | contains(); 106 | notContains(); 107 | isNull(); 108 | isNotNull(); 109 | between(); 110 | startsWith(); 111 | endsWith(); 112 | ``` 113 | 114 | ### Filter Deep 115 | 116 | @param path - as string by relation
117 | @param Operator "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in" | "notIn" | "contains" | "notContains" | "startsWith" | "endsWith"
118 | @param values can be string, number or array 119 | 120 | ```ts 121 | const { data, error, meta } = await strapiClient 122 | .from('students') 123 | .select(['firstname', 'lastname']) 124 | .filterDeep('address.city', 'eq', 'Munich') 125 | .get(); 126 | ``` 127 | 128 | ### Sort 129 | 130 | Expects an array with the field and order example - [{ field: 'id', order: 'asc' }] 131 | 132 | ```ts 133 | const { data, error, meta } = await strapiClient 134 | .from('students') 135 | .select(['firstname', 'lastname']) 136 | .between('id', [40, 45]) 137 | .sortBy([{ field: 'id', order: 'desc' }]) 138 | .get(); 139 | ``` 140 | 141 | ### Publication State 142 | 143 | Returns both draft entries & published entries. 144 | 145 | ```ts 146 | const { data, error, meta } = await strapiClient 147 | .from('students') 148 | .select(['firstname', 'lastname']) 149 | .withDraft() 150 | .get(); 151 | ``` 152 | 153 | Returns only draft entries. 154 | 155 | ```ts 156 | const { data, error, meta } = await strapiClient 157 | .from('students') 158 | .select(['firstname', 'lastname']) 159 | .onlyDraft() 160 | .get(); 161 | ``` 162 | 163 | ### Locale 164 | 165 | Get entries from a specific locale. 166 | 167 | ```ts 168 | const { data, error, meta } = await strapiClient 169 | .from('students') 170 | .select(['firstname', 'lastname']) 171 | .setLocale('de') 172 | .get(); 173 | ``` 174 | 175 | ### Pagination 176 | 177 | To paginate results by page 178 | 179 | ```ts 180 | const { data, error, meta } = await strapiClient 181 | .from('students') 182 | .select(['firstname', 'lastname']) 183 | .paginate(1, 15) 184 | .get(); 185 | ``` 186 | 187 | To paginate results by offset 188 | 189 | ```ts 190 | const { data, error, meta } = await strapiClient 191 | .from('students') 192 | .select(['firstname', 'lastname']) 193 | .paginateByOffset(0, 25) 194 | .get(); 195 | ``` 196 | 197 | ### Populate 198 | 199 | Populate 1 level for all relations 200 | 201 | ```ts 202 | const { data, error, meta } = await strapiClient 203 | .from('students') 204 | .select(['firstname', 'lastname']) 205 | .populate() 206 | .get(); 207 | ``` 208 | 209 | Populate 2 levels 210 | 211 | ```ts 212 | const { data, error, meta } = await strapiClient 213 | .from('students') 214 | .select(['firstname', 'lastname']) 215 | .populateWith
('address', ['id', 'city'], true) 216 | .get(); 217 | ``` 218 | 219 | Populate Deep 220 | 221 | ```ts 222 | const { data, error, meta } = await strapiClient 223 | .from('students') 224 | .select(['firstname', 'lastname']) 225 | .populateDeep([ 226 | { 227 | path: 'address', 228 | fields: ['id', 'string'], 229 | children: [{ key: 'country', fields: ['id', 'name'] }], 230 | }, 231 | ]) 232 | .get(); 233 | ``` 234 | 235 | # Post 236 | 237 | Create single record 238 | 239 | ```ts 240 | const { data, error, meta } = await strapiClient 241 | .from('students') 242 | .create({ firstname: 'Vorname', lastname: 'Nachname' }); 243 | ``` 244 | 245 | Create Many records 246 | 247 | ```ts 248 | const { success } = await strapiClient.from('students').createMany([ 249 | { firstname: 'muster', lastname: 'muster' }, 250 | { firstname: 'muster1', lastname: 'muster1' }, 251 | ]); 252 | ``` 253 | 254 | ### Available Post Methods 255 | 256 | ```ts 257 | update(); 258 | updateMany(); 259 | deleteOne(); 260 | deleteMany(); 261 | ``` 262 | 263 | # Auth 264 | 265 | signup new user 266 | 267 | ```ts 268 | const { data, error } = await strapiClient.auth.signUp({ 269 | username: 'username', 270 | email: 'name@gmail.com', 271 | password: '12345678', 272 | }); 273 | ``` 274 | 275 | signin user 276 | 277 | ```ts 278 | const { data, error } = await strapiClient.auth.signIn({ 279 | email: 'name@gmail.com', 280 | password: '12345678', 281 | }); 282 | ``` 283 | signout user - removes the authentication token if saved in localstorage 284 | 285 | ```ts 286 | const { error } = await strapiClient.auth.signOut(); 287 | ``` 288 | 289 | # Misc 290 | 291 | Get url from the client Object 292 | 293 | ```ts 294 | const url = strapiClient.getApiUrl(); 295 | 296 | console.log(url); 297 | 298 | ``` 299 | 300 | 301 | -------------------------------------------------------------------------------- /src/lib/strapi-filter-builder.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { generateQueryString, generateQueryFromRawString, stringToArray } from './helpers'; 3 | import { StrapiClientHelper } from './strapi-client-helper'; 4 | import { InferedTypeFromArray, PublicationState, StrapiApiResponse } from './types/base'; 5 | import { CrudSorting, PopulateDeepOptions, RelationalFilterOperators } from './types/crud'; 6 | 7 | export class StrapiFilterBuilder extends StrapiClientHelper { 8 | private httpClient: AxiosInstance; 9 | private normalizeData: boolean; 10 | private debug: boolean; 11 | 12 | constructor( 13 | url: string, 14 | axiosInstance: AxiosInstance, 15 | normalizeData: boolean, 16 | debug: boolean, 17 | private isNotUserContent: boolean 18 | ) { 19 | super(url); 20 | this.debug = debug; 21 | this.url = url; 22 | this.httpClient = axiosInstance; 23 | this.normalizeData = normalizeData; 24 | } 25 | 26 | async get(): Promise> { 27 | if (this.debug) { 28 | // eslint-disable-next-line no-console 29 | console.log(this.url); 30 | } 31 | return new Promise>((resolve) => { 32 | if (this.isNotUserContent) { 33 | this.httpClient 34 | .get>(this.url) 35 | .then((res) => { 36 | resolve(this.normalizeData ? this._returnDataHandler(res.data) : res.data); 37 | }) 38 | .catch((err) => { 39 | if (err) { 40 | resolve(this._returnErrorHandler(err)); 41 | } 42 | }); 43 | } 44 | if (!this.isNotUserContent) { 45 | this.httpClient 46 | .get(this.url) 47 | .then((res) => { 48 | resolve({ data: res.data, meta: undefined }); 49 | }) 50 | .catch((err) => { 51 | if (err) { 52 | resolve(this._returnErrorHandler(err)); 53 | } 54 | }); 55 | } 56 | }); 57 | } 58 | 59 | equalTo(field: keyof InferedTypeFromArray, value: string | number) { 60 | this.url = this._generateFilter({ 61 | field, 62 | operator: 'eq', 63 | value, 64 | }); 65 | return this; 66 | } 67 | 68 | notEqualTo(field: keyof InferedTypeFromArray, value: string | number) { 69 | this.url = this._generateFilter({ 70 | field, 71 | operator: 'ne', 72 | value, 73 | }); 74 | return this; 75 | } 76 | 77 | lessThan(field: keyof InferedTypeFromArray, value: string | number) { 78 | this.url = this._generateFilter({ 79 | field, 80 | operator: 'lt', 81 | value, 82 | }); 83 | return this; 84 | } 85 | 86 | lessThanOrEqualTo(field: keyof InferedTypeFromArray, value: string | number) { 87 | this.url = this._generateFilter({ 88 | field, 89 | operator: 'lte', 90 | value, 91 | }); 92 | return this; 93 | } 94 | 95 | greaterThan(field: keyof InferedTypeFromArray, value: string | number) { 96 | this.url = this._generateFilter({ 97 | field, 98 | operator: 'gt', 99 | value, 100 | }); 101 | return this; 102 | } 103 | 104 | greaterThanOrEqualTo(field: keyof InferedTypeFromArray, value: string | number) { 105 | this.url = this._generateFilter({ 106 | field, 107 | operator: 'gte', 108 | value, 109 | }); 110 | return this; 111 | } 112 | 113 | containsCaseSensitive(field: keyof InferedTypeFromArray, value: string) { 114 | this.url = this._generateFilter({ 115 | field, 116 | operator: 'contains', 117 | value, 118 | }); 119 | return this; 120 | } 121 | 122 | notContainsCaseSensitive(field: keyof InferedTypeFromArray, value: string) { 123 | this.url = this._generateFilter({ 124 | field, 125 | operator: 'notContains', 126 | value, 127 | }); 128 | return this; 129 | } 130 | 131 | contains(field: keyof InferedTypeFromArray, value: string) { 132 | this.url = this._generateFilter({ 133 | field, 134 | operator: 'containsi', 135 | value, 136 | }); 137 | return this; 138 | } 139 | 140 | notContains(field: keyof InferedTypeFromArray, value: string) { 141 | this.url = this._generateFilter({ 142 | field, 143 | operator: 'notContainsi', 144 | value, 145 | }); 146 | return this; 147 | } 148 | 149 | isNull(field: keyof InferedTypeFromArray, value: string) { 150 | this.url = this._generateFilter({ 151 | field, 152 | operator: 'null', 153 | value, 154 | }); 155 | return this; 156 | } 157 | 158 | isNotNull(field: keyof InferedTypeFromArray, value: string) { 159 | this.url = this._generateFilter({ 160 | field, 161 | operator: 'notNull', 162 | value, 163 | }); 164 | return this; 165 | } 166 | 167 | between(field: keyof InferedTypeFromArray, value: Array) { 168 | this.url = this._generateFilter({ 169 | field, 170 | operator: 'between', 171 | value, 172 | }); 173 | return this; 174 | } 175 | 176 | startsWith(field: keyof InferedTypeFromArray, value: string) { 177 | this.url = this._generateFilter({ 178 | field, 179 | operator: 'startsWith', 180 | value, 181 | }); 182 | return this; 183 | } 184 | 185 | endsWith(field: keyof InferedTypeFromArray, value: string) { 186 | this.url = this._generateFilter({ 187 | field, 188 | operator: 'endsWith', 189 | value, 190 | }); 191 | return this; 192 | } 193 | 194 | /** 195 | * 196 | * @param path relation path as string type. Ex - 'subcategories.products.slug' 197 | * @param operator "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in" | "notIn" | "contains" | "notContains" | "startsWith" | "endsWith" 198 | * @param value values can be string, number or array 199 | * @returns 200 | */ 201 | filterDeep(path: string, operator: RelationalFilterOperators, value: string | number | Array) { 202 | this.url = this._genrateRelationsFilter({ path: stringToArray(path), operator, value }); 203 | return this; 204 | } 205 | 206 | /** 207 | * 208 | * @param sort expects an array with the field and order example - [{ field: 'id', order: 'asc' }] 209 | * 210 | */ 211 | sortBy(sort: CrudSorting>) { 212 | this.url = this._generateSort(sort); 213 | return this; 214 | } 215 | 216 | /** 217 | * 218 | * @param page Page number 219 | * @param pageSize Page size 220 | * @returns Pagination by page 221 | */ 222 | paginate(page: number, pageSize: number) { 223 | const paginateRawQuery = `pagination[page]=${page}&pagination[pageSize]=${pageSize}`; 224 | this.url = this._handleUrl(generateQueryFromRawString(paginateRawQuery)); 225 | return this; 226 | } 227 | 228 | /** 229 | * 230 | * @param start Start value (i.e. first entry to return) 231 | * @param limit Number of entries to return 232 | * @returns Pagination by offset 233 | */ 234 | paginateByOffset(start: number, limit: number) { 235 | const paginateRawQuery = `pagination[start]=${start}&pagination[limit]=${limit}`; 236 | this.url = this._handleUrl(generateQueryFromRawString(paginateRawQuery)); 237 | return this; 238 | } 239 | 240 | /** 241 | * 242 | * @returns returns both draft entries & published entries 243 | */ 244 | withDraft() { 245 | this.url = this._handleUrl(`publicationState=${PublicationState.PREVIEW}`); 246 | return this; 247 | } 248 | 249 | /** 250 | * 251 | * @returns retrieve only draft entries 252 | */ 253 | onlyDraft() { 254 | this.url = this._handleUrl(`publicationState=${PublicationState.PREVIEW}&filters[publishedAt][$null]=true`); 255 | return this; 256 | } 257 | 258 | /** 259 | * 260 | * @param localeCode expects string locale-code 261 | * @returns returns content only for a specified locale 262 | */ 263 | setLocale(localeCode: string) { 264 | this.url = this._handleUrl(`locale=${localeCode}`); 265 | return this; 266 | } 267 | 268 | /** 269 | * 270 | * @returns Populate 1 level for all relations 271 | */ 272 | populate() { 273 | const obj = { 274 | populate: '*', 275 | }; 276 | this.url = this._handleUrl(generateQueryString(obj)); 277 | return this; 278 | } 279 | 280 | /** 281 | * @param key relation name 282 | * @param selectFields an Array of field names to populate 283 | * @param level2 expects boolean value to To populate second-level deep for all relations 284 | */ 285 | 286 | populateWith( 287 | relation: T extends Array ? keyof U : keyof T, 288 | selectFields?: Array, 289 | level2?: boolean 290 | ) { 291 | const obj = { 292 | populate: { 293 | [relation]: { 294 | fields: selectFields, 295 | populate: level2 ? '*' : null, 296 | }, 297 | }, 298 | }; 299 | this.url = this._handleUrl(generateQueryString(obj)); 300 | return this; 301 | } 302 | 303 | /** 304 | * 305 | * @param populateDeepValues expects an array with the path, fields and children 306 | * @type path: string 307 | * 308 | * @type fields: Array of strings 309 | * 310 | * @type children : Array [key:string, fields:Array of strings] 311 | 312 | * @returns Populate n level for the specified relation 313 | */ 314 | populateDeep(populateDeepValues: PopulateDeepOptions[]) { 315 | this.url = this._generatePopulateDeep(populateDeepValues); 316 | return this; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 74 | 75 | /* Type Checking */ 76 | "strict": true /* Enable all strict type-checking options. */, 77 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | --------------------------------------------------------------------------------