mirror of https://github.com/BRAVO68WEB/shx.git
Initial commit
This commit is contained in:
commit
9e52e0391d
|
@ -0,0 +1,17 @@
|
|||
PORT=
|
||||
NODE_ENV=
|
||||
HASURA_GRAPHQL_ADMIN_SECRET=
|
||||
HASURA_GRAPHQL_ENDPOINT=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_REGION=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USER=
|
||||
MAIL_PASS=
|
||||
MAIL_LOGGER=
|
||||
MAIL_FROM_EMAIL=
|
||||
MAIL_FROM_NAME=
|
||||
CACHE_ENV=
|
||||
REDIS_URL=
|
|
@ -0,0 +1,124 @@
|
|||
# Webstorm stuff
|
||||
.idea
|
||||
|
||||
# Ignoring Build folder
|
||||
build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
|
||||
# lock files
|
||||
yarn.lock
|
||||
*-lock.json
|
||||
|
||||
# Custom
|
||||
testBox.js
|
||||
jwtRS256.key
|
||||
jwtRS256.key.pub
|
||||
|
||||
#markdown files
|
||||
**/*.md
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.husky
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
|
@ -0,0 +1,121 @@
|
|||
# Webstorm stuff
|
||||
.idea
|
||||
|
||||
# Ignoring Build folder
|
||||
build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
|
||||
# lock files
|
||||
yarn.lock
|
||||
*-lock.json
|
||||
|
||||
# Custom
|
||||
testBox.js
|
||||
jwtRS256.key
|
||||
jwtRS256.key.pub
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "always"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD [ "yarn", "start" ]
|
|
@ -0,0 +1,15 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2023 Jyotirmoy Bandopadhayaya (https://b68.dev)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,47 @@
|
|||
🚀 **typescript-express-hasura-pgsql-template**
|
||||
|
||||
A template repo to quickly start developing a backend with Express JS, Typescript, Redis, Hasura GraphQL (with Postgres), Husky, Nodemailer, and AWS S3 preconfigured.
|
||||
|
||||
## 📝 Description
|
||||
|
||||
This repo provides a starting point for developing a backend with a modern tech stack.
|
||||
|
||||
## 🛠️ Technologies Used
|
||||
|
||||
- Express JS
|
||||
- Typescript
|
||||
- Redis
|
||||
- Hasura GraphQL (with Postgres)
|
||||
- Husky
|
||||
- Nodemailer
|
||||
- AWS S3
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started with the project, follow these steps:
|
||||
|
||||
1. Clone the repo.
|
||||
2. Run `npm install` to install dependencies.
|
||||
3. Copy the `.env.example` file and create a `.env` file with your environment variables.
|
||||
4. Run `npm run dev` to start the development server.
|
||||
|
||||
## 📜 Scripts
|
||||
|
||||
- `npm run dev`: Starts the development server for both Hasura and Express.
|
||||
- `npm run dev:hasura`: Starts the Hasura development server.
|
||||
- `npm run dev:express`: Starts the Express development server.
|
||||
- `npm run build`: Builds the project.
|
||||
- `npm start`: Starts the project.
|
||||
- `npm run prettier`: Runs Prettier to format code.
|
||||
- `npm run prepare`: Installs Husky.
|
||||
- `npm run configure-husky`: Configures Husky.
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the ISC License.
|
||||
|
||||
For more information, please see the `LICENSE` file.
|
||||
|
||||
## 📧 Contact
|
||||
|
||||
If you have any questions or would like to contribute to the project, please contact `hi@b68.dev`.
|
|
@ -0,0 +1,59 @@
|
|||
import fs from 'fs';
|
||||
import { parse as parseFile } from 'envfile';
|
||||
|
||||
type IconfigStore = 'development' | 'production';
|
||||
|
||||
export interface IConfigKeys {
|
||||
PORT: string | number;
|
||||
NODE_ENV: string;
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: string;
|
||||
HASURA_GRAPHQL_ENDPOINT: string;
|
||||
AWS_S3_BUCKET: string;
|
||||
AWS_REGION: string;
|
||||
AWS_ACCESS_KEY_ID: string;
|
||||
AWS_SECRET_ACCESS_KEY: string;
|
||||
MAIL_HOST: string;
|
||||
MAIL_PORT: number;
|
||||
MAIL_USER: string;
|
||||
MAIL_PASS: string;
|
||||
MAIL_LOGGER: boolean;
|
||||
MAIL_FROM_EMAIL: string;
|
||||
MAIL_FROM_NAME: string;
|
||||
CACHE_ENV: string;
|
||||
REDIS_URL: string;
|
||||
}
|
||||
|
||||
export default class ConfigStoreFactory {
|
||||
public configStoreType: IconfigStore;
|
||||
|
||||
constructor(isProd = false) {
|
||||
if (isProd) {
|
||||
this.configStoreType = 'production';
|
||||
} else {
|
||||
this.configStoreType = 'development';
|
||||
}
|
||||
}
|
||||
|
||||
public async getConfigStore() {
|
||||
if (this.configStoreType === 'development') {
|
||||
const envContent = await fs.readFileSync(`./.env`, 'utf8');
|
||||
const env: Partial<IConfigKeys> = await parseFile(envContent);
|
||||
return env;
|
||||
} else {
|
||||
let reqEnvContent: any = await fs.readFileSync('./.env.example', 'utf8');
|
||||
reqEnvContent = reqEnvContent.replaceAll('=', '');
|
||||
reqEnvContent = reqEnvContent.split('\n');
|
||||
const missingKeys: string[] = [];
|
||||
const env: Partial<IConfigKeys> = {};
|
||||
for (const line of reqEnvContent) {
|
||||
if (!process.env[line]) {
|
||||
missingKeys.push(line);
|
||||
} else env[line] = process.env[line];
|
||||
}
|
||||
if (missingKeys.length > 0) {
|
||||
throw new Error(`Missing keys: ${missingKeys}`);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import DevClass from '../services/dev.service';
|
||||
import { Request, Response } from 'express';
|
||||
import { makeResponse } from '../libs';
|
||||
|
||||
export default class DevController extends DevClass {
|
||||
public devFunc = async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const data = await this.devRun();
|
||||
res.send(makeResponse(data));
|
||||
} catch (err: any) {
|
||||
res.send(makeResponse(err.message, {}, 'Failed', true));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
version: 3
|
||||
endpoint: <your-endpoint>
|
||||
metadata_directory: metadata
|
||||
actions:
|
||||
kind: synchronous
|
||||
handler_webhook_baseurl: http://localhost:3000
|
|
@ -0,0 +1,6 @@
|
|||
actions: []
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
version: 3
|
|
@ -0,0 +1,31 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const axiosInstance = axios.create({});
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
config => {
|
||||
const newConfig: any = { ...config };
|
||||
newConfig.metadata = { startTime: new Date() };
|
||||
return newConfig;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => {
|
||||
const newRes: any = { ...response };
|
||||
newRes.config.metadata.endTime = new Date();
|
||||
newRes.duration =
|
||||
newRes.config.metadata.endTime - newRes.config.metadata.startTime;
|
||||
return newRes;
|
||||
},
|
||||
error => {
|
||||
const newError = { ...error };
|
||||
newError.config.metadata.endTime = new Date();
|
||||
newError.duration =
|
||||
newError.config.metadata.endTime - newError.config.metadata.startTime;
|
||||
return Promise.reject(newError);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,65 @@
|
|||
import * as redis from 'redis';
|
||||
import NodeCache from 'node-cache';
|
||||
import { configKeys } from '..';
|
||||
|
||||
type CacheEnvironment = 'development' | 'production';
|
||||
export default class CacheClient {
|
||||
private static _clientMode: CacheEnvironment;
|
||||
private static _redisClient: redis.RedisClientType;
|
||||
private static _nodeClient: NodeCache;
|
||||
|
||||
static get client() {
|
||||
return this._clientMode === 'production'
|
||||
? this._redisClient
|
||||
: this._nodeClient;
|
||||
}
|
||||
|
||||
static get env() {
|
||||
return this._clientMode;
|
||||
}
|
||||
|
||||
static init(forceEnv?: CacheEnvironment) {
|
||||
const env =
|
||||
forceEnv ||
|
||||
configKeys.CACHE_ENV ||
|
||||
configKeys.NODE_ENV ||
|
||||
'development';
|
||||
|
||||
if (!['development', 'production'].includes(env))
|
||||
throw new Error(
|
||||
"Invalid Caching Environment, expected - ['development', 'production'], received - " +
|
||||
env
|
||||
);
|
||||
|
||||
this._clientMode = env as CacheEnvironment;
|
||||
|
||||
const redisUrl = configKeys.REDIS_URL || '';
|
||||
|
||||
if (env === 'production') {
|
||||
this._redisClient = redis.createClient({
|
||||
url: redisUrl,
|
||||
name: '<>', // TODO: add redis name
|
||||
});
|
||||
this._redisClient.connect();
|
||||
}
|
||||
|
||||
this._nodeClient = new NodeCache();
|
||||
console.log(`Caching Client initialized in '${env}' environment`);
|
||||
}
|
||||
|
||||
static async set(key: string, value: any) {
|
||||
if (this._clientMode === 'production') {
|
||||
await this._redisClient.set(key, value);
|
||||
} else {
|
||||
this._nodeClient.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
static async get(key: string): Promise<string | null> {
|
||||
if (this._clientMode === 'production') {
|
||||
return await this._redisClient.get(key);
|
||||
} else {
|
||||
return (this._nodeClient.get(key) as string) || null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { GraphQLClient } from 'graphql-request';
|
||||
import { configKeys } from '..';
|
||||
|
||||
export let client = new GraphQLClient('');
|
||||
|
||||
export const hgqlInit = () => {
|
||||
console.log('\n🚀 GraphQL Client Initialized');
|
||||
|
||||
let HASURA_URL: string = configKeys.HASURA_GRAPHQL_ENDPOINT || '';
|
||||
HASURA_URL += HASURA_URL.endsWith('/') ? 'v1/graphql' : '/v1/graphql';
|
||||
const HASURA_ADMIN: string = configKeys.HASURA_GRAPHQL_ADMIN_SECRET || '';
|
||||
|
||||
client = new GraphQLClient(HASURA_URL, {
|
||||
headers: {
|
||||
'x-hasura-admin-secret': HASURA_ADMIN,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export * from './cache.factory';
|
||||
export * from './gql_clent';
|
||||
export * from './axios_client';
|
||||
export * from './upload.factory'
|
|
@ -0,0 +1,50 @@
|
|||
import { configKeys } from "..";
|
||||
|
||||
export interface MailConfig {
|
||||
host?: string;
|
||||
port?: number;
|
||||
secure?: boolean;
|
||||
auth?: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
logger?: boolean;
|
||||
}
|
||||
|
||||
type MailerConfigValues = {
|
||||
[k: string]: MailConfig & Partial<ExtraMailerConfig>;
|
||||
};
|
||||
|
||||
interface ExtraMailerConfig {
|
||||
from_email: string;
|
||||
from_name: string;
|
||||
}
|
||||
|
||||
const ConfigValue: MailerConfigValues = {
|
||||
development: {
|
||||
host: configKeys.MAIL_HOST,
|
||||
port: configKeys.MAIL_PORT,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: configKeys.MAIL_USER,
|
||||
pass: configKeys.MAIL_PASS,
|
||||
},
|
||||
logger: configKeys.MAIL_LOGGER,
|
||||
from_email: configKeys.MAIL_FROM_EMAIL,
|
||||
from_name: configKeys.MAIL_FROM_NAME,
|
||||
},
|
||||
production: {
|
||||
host: configKeys.MAIL_HOST,
|
||||
port: configKeys.MAIL_PORT,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: configKeys.MAIL_USER,
|
||||
pass: configKeys.MAIL_PASS,
|
||||
},
|
||||
logger: configKeys.MAIL_LOGGER,
|
||||
from_email: configKeys.MAIL_FROM_EMAIL,
|
||||
from_name: configKeys.MAIL_FROM_NAME,
|
||||
},
|
||||
};
|
||||
|
||||
export default ConfigValue;
|
|
@ -0,0 +1,20 @@
|
|||
import nodemailer from 'nodemailer';
|
||||
import Mail from 'nodemailer/lib/mailer';
|
||||
import { configKeys } from '..';
|
||||
|
||||
import MailerConfig from './mailer.config';
|
||||
|
||||
const mailConfig =
|
||||
configKeys.NODE_ENV === 'production'
|
||||
? MailerConfig.production
|
||||
: MailerConfig.development;
|
||||
|
||||
const transporter = nodemailer.createTransport(mailConfig);
|
||||
|
||||
export default async (mail: Mail.Options): Promise<void> => {
|
||||
try {
|
||||
await transporter.sendMail(mail);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
import multer from 'multer';
|
||||
import multerS3 from 'multer-s3';
|
||||
import path from 'path';
|
||||
import napiNanoId from 'napi-nanoid';
|
||||
import { configKeys } from '..';
|
||||
interface UploadFactoryOptions {
|
||||
region: string;
|
||||
bucket: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
}
|
||||
interface UploaderConfig {
|
||||
folder: string;
|
||||
mimeFilters: string[];
|
||||
}
|
||||
export class UploadFactory {
|
||||
private options: UploadFactoryOptions & Partial<UploaderConfig>;
|
||||
private s3Client: S3Client;
|
||||
|
||||
constructor(options?: Partial<UploadFactoryOptions>) {
|
||||
this.options = {
|
||||
bucket: options?.bucket || configKeys.AWS_S3_BUCKET || '',
|
||||
region: options?.region || configKeys.AWS_REGION || '',
|
||||
accessKey: options?.accessKey || configKeys.AWS_ACCESS_KEY_ID || '',
|
||||
secretKey: options?.secretKey || configKeys.AWS_SECRET_ACCESS_KEY || '',
|
||||
};
|
||||
|
||||
this.s3Client = new S3Client({
|
||||
region: this.options.region,
|
||||
credentials: {
|
||||
accessKeyId: this.options.accessKey,
|
||||
secretAccessKey: this.options.secretKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public get serviceName(): string {
|
||||
return 'aws:' + this.options.bucket;
|
||||
}
|
||||
|
||||
public getUploader(config?: Partial<UploadFactoryOptions & UploaderConfig>) {
|
||||
const finalOptions = {
|
||||
...this.options,
|
||||
...(config || {}),
|
||||
};
|
||||
|
||||
return multer({
|
||||
fileFilter(_req, file, cb) {
|
||||
const res = finalOptions.mimeFilters
|
||||
? finalOptions.mimeFilters.includes(file.mimetype)
|
||||
: true;
|
||||
cb(null, res);
|
||||
},
|
||||
storage: multerS3({
|
||||
s3: this.s3Client,
|
||||
bucket: this.options.bucket,
|
||||
acl: 'public-read',
|
||||
contentType: multerS3.AUTO_CONTENT_TYPE,
|
||||
metadata: function (_req, file, cb) {
|
||||
const meta = {
|
||||
fieldName: file.fieldname,
|
||||
fileName: file.originalname,
|
||||
uploadOn: new Date().toISOString(),
|
||||
};
|
||||
cb(null, meta);
|
||||
},
|
||||
key: function (_req, file, cb) {
|
||||
const key: string[] = [];
|
||||
if (finalOptions.folder) key.push(finalOptions.folder);
|
||||
const value = napiNanoId.nanoid();
|
||||
const ext = path.extname(file.originalname);
|
||||
key.push(value + ext);
|
||||
|
||||
cb(null, key.join('/'));
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import 'dotenv/config';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import helmet from 'helmet';
|
||||
|
||||
import { hgqlInit } from './helpers';
|
||||
import routes from './routes';
|
||||
import { errorHandler, notFoundHandler } from './libs';
|
||||
import pkg from './package.json' assert { type: 'json' };
|
||||
import configStore, { IConfigKeys } from './configs';
|
||||
|
||||
export const app: express.Application = express();
|
||||
|
||||
console.log('🚀', '@' + pkg.author.name + '/' + pkg.name, 'v' + pkg.version);
|
||||
|
||||
const isDev: boolean = process.env.NODE_ENV == 'production';
|
||||
console.log(isDev ? '🚀 Production Mode' : '🚀 Development Mode');
|
||||
const configs = new configStore(isDev);
|
||||
const configKeys: IConfigKeys = await configs.getConfigStore() as IConfigKeys;
|
||||
|
||||
hgqlInit();
|
||||
|
||||
app.use(cors());
|
||||
app.use(helmet());
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
app.use('/health', (req, res) => {
|
||||
return res.status(200).json({
|
||||
app: pkg.name,
|
||||
request_ip: req.ip,
|
||||
uptime: process.uptime(),
|
||||
hrtime: process.hrtime(),
|
||||
});
|
||||
});
|
||||
|
||||
console.log('☄', 'Base Route', '/');
|
||||
app.use('/', routes);
|
||||
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(configKeys.PORT, async () => {
|
||||
console.log(`\nServer running on port ${configKeys.PORT}`);
|
||||
});
|
||||
|
||||
export { configKeys };
|
|
@ -0,0 +1,29 @@
|
|||
import { Response } from 'express';
|
||||
import { ClientError } from 'graphql-request';
|
||||
import { CustomError } from './error';
|
||||
import { ValidationError } from 'joi';
|
||||
|
||||
const customErrorHandler = async (res: Response, error: any) => {
|
||||
if (error instanceof ValidationError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Data validation failed',
|
||||
details: error.details,
|
||||
});
|
||||
}
|
||||
if (error instanceof CustomError) {
|
||||
return res
|
||||
.status(error.statusCode)
|
||||
.send({ success: false, message: error.message, data: error.data });
|
||||
}
|
||||
if (error instanceof ClientError) {
|
||||
const { errors = [] } = error.response;
|
||||
const [err] = errors;
|
||||
if (err?.message) {
|
||||
return res.status(422).send({ success: false, message: err.message });
|
||||
}
|
||||
}
|
||||
res.status(500).send({ success: false, message: 'Internal ServerError.' });
|
||||
};
|
||||
|
||||
export default customErrorHandler;
|
|
@ -0,0 +1,24 @@
|
|||
export class CustomError extends Error {
|
||||
public statusCode: number;
|
||||
public data: any;
|
||||
|
||||
constructor(args: { message?: string; statusCode?: number; data?: any }) {
|
||||
super(args.message);
|
||||
this.statusCode = args.statusCode || 500;
|
||||
this.data = args.data;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return {
|
||||
message: this.message,
|
||||
statusCode: this.statusCode,
|
||||
data: this.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends CustomError {
|
||||
constructor() {
|
||||
super({ message: 'NOT_FOUND', statusCode: 404 });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './middleware';
|
||||
export * from './utilities';
|
|
@ -0,0 +1,65 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import Joi from 'joi';
|
||||
import { CustomError, NotFoundError } from './error';
|
||||
import { pick } from './utilities';
|
||||
import { configKeys } from ".."
|
||||
|
||||
export const errorHandler = async (
|
||||
err: any,
|
||||
_req: Request,
|
||||
res: Response,
|
||||
// eslint-disable-next-line
|
||||
_next: NextFunction
|
||||
) => {
|
||||
if ('statusCode' in err) {
|
||||
return res.status(err.statusCode).json({
|
||||
message: err.message,
|
||||
error: true,
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
return res.status(500).json({
|
||||
message: err.message,
|
||||
error: true,
|
||||
data: null,
|
||||
error_stack: configKeys.NODE_ENV === 'production' ? undefined : err.stack,
|
||||
});
|
||||
};
|
||||
|
||||
export const notFoundHandler = async (
|
||||
_req: Request,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
return next(new NotFoundError());
|
||||
};
|
||||
|
||||
export const validate =
|
||||
(schema: any) => (req: Request, _res: Response, next: NextFunction) => {
|
||||
if (Object.keys(req.body).length !== 0 && !req.is('application/json')) {
|
||||
return next(new Error('Supports JSON request body only'));
|
||||
}
|
||||
|
||||
const validSchema = pick(schema, ['params', 'query', 'body']);
|
||||
|
||||
const object = pick(req, Object.keys(validSchema));
|
||||
|
||||
const { value, error } = Joi.compile(validSchema)
|
||||
.prefs({ errors: { label: 'key' } })
|
||||
.validate(object);
|
||||
if (error) {
|
||||
const errorMessage = error.details
|
||||
.map(details => details.message)
|
||||
.join(', ');
|
||||
return next(
|
||||
new CustomError({
|
||||
message: errorMessage,
|
||||
statusCode: 400,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(req, value);
|
||||
|
||||
return next();
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
import { PaginationType } from '../types';
|
||||
|
||||
export const makeResponse = (
|
||||
data: any,
|
||||
meta_data: any = null,
|
||||
message = 'Success',
|
||||
error = false
|
||||
) => ({
|
||||
message,
|
||||
error,
|
||||
meta_data,
|
||||
data,
|
||||
});
|
||||
|
||||
const joinPrefix = (...keys: string[]) => keys.join('_');
|
||||
|
||||
export const flattenObject = (obj: any, prefix = '') => {
|
||||
let newObj: any = {};
|
||||
for (const key in obj) {
|
||||
const pfx = prefix ? joinPrefix(prefix, key) : key;
|
||||
if (obj[key] instanceof Object) {
|
||||
newObj = { ...newObj, ...flattenObject(obj[key], pfx) };
|
||||
} else {
|
||||
newObj = { ...newObj, [pfx]: obj[key] };
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
};
|
||||
|
||||
export const cleanObject = (obj: any) => {
|
||||
const newObj: any = obj;
|
||||
for (const k in obj) {
|
||||
if (
|
||||
(!k || !obj[k] || typeof k === 'undefined') &&
|
||||
typeof obj[k] !== 'boolean' &&
|
||||
obj[k] !== 0
|
||||
)
|
||||
delete obj[k];
|
||||
}
|
||||
return newObj;
|
||||
};
|
||||
|
||||
export const cleanObjectKeepNull = (obj: any) => {
|
||||
const newObj: any = obj;
|
||||
for (const k in obj) {
|
||||
if (
|
||||
(!k || !obj[k] || typeof k === 'undefined') &&
|
||||
typeof obj[k] !== 'boolean' &&
|
||||
obj[k] !== 0 &&
|
||||
obj[k] !== null
|
||||
)
|
||||
delete obj[k];
|
||||
}
|
||||
return newObj;
|
||||
};
|
||||
|
||||
export const paginateRequest = (q: any): PaginationType => {
|
||||
const filter_keys = Object.keys(q).filter(c => c.startsWith('filter_'));
|
||||
const filters = filter_keys.length
|
||||
? filter_keys
|
||||
.map(filter_key => {
|
||||
const filter_subset = filter_key.replace('filter_', '').split('.');
|
||||
let mode = typeof q[filter_key] === 'number' ? '_eq' : '_iregex';
|
||||
if (q[filter_key].includes('-')) {
|
||||
mode = '_eq';
|
||||
}
|
||||
|
||||
return parseFilter(filter_subset, q[filter_key], 0, mode);
|
||||
})
|
||||
.reduceRight((agg, cur) => {
|
||||
const [cur_key] = Object.keys(cur);
|
||||
if (cur_key in agg) {
|
||||
if (Array.isArray(agg[cur_key])) {
|
||||
agg[cur_key].push(cur);
|
||||
} else {
|
||||
cur[cur_key] = [cur[cur_key], agg[cur_key]];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
return {
|
||||
...agg,
|
||||
...cur,
|
||||
};
|
||||
}, {})
|
||||
: undefined;
|
||||
return {
|
||||
page: parseInt(q.page) || 0,
|
||||
limit: parseInt(q.limit || q.items) || 50,
|
||||
sort_by: q.sort_by,
|
||||
sort_order: q.sort_order || 'asc',
|
||||
filters,
|
||||
} as PaginationType;
|
||||
};
|
||||
|
||||
export const parseFilter = (
|
||||
filter: string[],
|
||||
value: string,
|
||||
index = 0,
|
||||
filterMode = '_iregex'
|
||||
) => {
|
||||
let fx: any = { [filterMode]: value };
|
||||
if (index < filter.length - 1) {
|
||||
fx = parseFilter(filter, value, index + 1, filterMode);
|
||||
}
|
||||
|
||||
const key = filter[index];
|
||||
return { [key]: fx };
|
||||
};
|
||||
|
||||
export const subtractHours = (date: Date, hours: number) => {
|
||||
date.setHours(date.getHours() - hours);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
export const capitalizeEachWord = (str: string) => {
|
||||
return str
|
||||
.split(' ')
|
||||
.map(word =>
|
||||
!word.length ? '' : word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export const pick = (object: any, keys: any) => {
|
||||
return keys.reduce((obj: any, key: any) => {
|
||||
if (object && key in object) {
|
||||
obj[key] = object[key];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getSortColumn = (
|
||||
pg_sort_by?: string,
|
||||
def = 'id',
|
||||
options: string[] = []
|
||||
) => {
|
||||
pg_sort_by ||= def;
|
||||
return options.includes(pg_sort_by) ? pg_sort_by : def;
|
||||
};
|
||||
|
||||
export const is_uuid = (value: string) => {
|
||||
const regex = /^()/;
|
||||
return regex.test(value);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "typescript-express-hasura-pgsql-template",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend Repo Template",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/<username>/<repo-name>.git",
|
||||
"author": {
|
||||
"email": "<email>",
|
||||
"name": "<name>",
|
||||
"url": "<url>"
|
||||
},
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.226.0",
|
||||
"axios": "^1.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"envfile": "^6.18.0",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^5.0.0",
|
||||
"helmet": "^6.0.1",
|
||||
"joi": "^17.7.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"napi-nanoid": "^0.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"redis": "^4.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:express\" \"npm run dev:hasura\"",
|
||||
"dev:hasura": "cd hasura && hasura --skip-update-check --envfile ../.env console",
|
||||
"dev:express": "cross-env NODE_ENV=development nodemon -x node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm index.ts --signal SIGKILL --ignore node_modules",
|
||||
"build": "tsc",
|
||||
"start": "node --es-module-specifier-resolution=node --loader ts-node/esm ./build/index.js",
|
||||
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\"",
|
||||
"prepare": "husky install",
|
||||
"configure-husky": "npx husky install && npx husky add .husky/pre-commit \"npx --no-install lint-staged\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.23",
|
||||
"@swc/wasm": "^1.3.23",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"hasura-cli": "^2.15.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.1.2",
|
||||
"nodemon": "^2.0.22",
|
||||
"prettier": "^2.8.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,json,ts,css}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { Router } from 'express';
|
||||
import DevController from '../controllers/dev.controller';
|
||||
|
||||
const { devFunc } = new DevController();
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', devFunc);
|
||||
|
||||
router.all('/err', async (req, res, next) => {
|
||||
try {
|
||||
throw new Error('This is an error');
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,40 @@
|
|||
import path from 'path';
|
||||
import { readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const isCompiled = path.extname(__filename) === '.js';
|
||||
const thisFileName = path.basename(__filename);
|
||||
|
||||
const loadRoutes = async (dirPath: string, prefix = '/') => {
|
||||
readdirSync(dirPath, {
|
||||
withFileTypes: true,
|
||||
}).forEach(async f => {
|
||||
if (f.isFile()) {
|
||||
if (f.name == thisFileName) return;
|
||||
|
||||
const isRouteMod = f.name.endsWith(`.routes.${isCompiled ? 'js' : 'ts'}`);
|
||||
if (isRouteMod) {
|
||||
const route = f.name.replace(`.routes.${isCompiled ? 'js' : 'ts'}`, '');
|
||||
const modRoute = path.join(prefix, route);
|
||||
console.log('🛰️', 'Loaded', modRoute);
|
||||
|
||||
const mod = await import(path.join(baseDir, prefix + f.name));
|
||||
router.use(modRoute, mod.default);
|
||||
}
|
||||
} else if (f.isDirectory()) {
|
||||
await loadRoutes(path.resolve(dirPath, f.name), prefix + f.name + '/');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let baseDir = path.dirname(__filename);
|
||||
baseDir = path.resolve(baseDir);
|
||||
|
||||
loadRoutes(baseDir);
|
||||
export default router;
|
|
@ -0,0 +1,5 @@
|
|||
export default class DevClass {
|
||||
public devRun = async (): Promise<any> => {
|
||||
return 'Hello World!';
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2018", "es5", "dom"],
|
||||
"typeRoots": ["node_modules/@types", "./types"],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2017",
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"exclude": ["./node_modules/**/*", "./build/**/*"],
|
||||
"include": ["./**/*.ts", "./**/*.tsx", "./**/*.json", "./**/*.js"],
|
||||
"ts-node": {
|
||||
"swc": true
|
||||
},
|
||||
"files": ["types/index.d.ts"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface PaginationType {
|
||||
page: number;
|
||||
limit: number;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
sort_by?: string;
|
||||
filters?: { [k: string]: any };
|
||||
}
|
Loading…
Reference in New Issue