From 34fffec510b7686e77d0c084b74f4da4ec05c903 Mon Sep 17 00:00:00 2001 From: fruneen Date: Tue, 26 Mar 2024 13:10:36 +0300 Subject: [PATCH] feat: add shared linters --- .vscode/settings.json | 5 - deploy/aws/deploy/script/Dockerfile | 2 +- deploy/digital-ocean/deploy/script/Dockerfile | 2 +- docs/deployment/kubernetes/overview.mdx | 4 +- docs/web/styling.mdx | 2 +- template/.dockerignore | 29 +- template/.gitignore | 28 +- template/.vscode/settings.json | 5 - template/apps/api/.dockerignore | 24 +- template/apps/api/.eslintignore | 25 +- template/apps/api/.eslintrc.js | 59 +- template/apps/api/.gitignore | 28 +- template/apps/api/.prettierignore | 23 + template/apps/api/.prettierrc.json | 1 + template/apps/api/README.md | 14 +- template/apps/api/jest.config.ts | 4 +- template/apps/api/package.json | 21 +- template/apps/api/src/app.ts | 72 +- .../src/config/retool/users-management.json | 169 +- template/apps/api/src/db.ts | 2 +- template/apps/api/src/globals.d.ts | 2 +- template/apps/api/src/io-emitter.ts | 3 +- template/apps/api/src/koa-qs.d.ts | 2 +- template/apps/api/src/middlewares/index.ts | 7 +- .../src/middlewares/validate.middleware.ts | 4 +- template/apps/api/src/migrator.ts | 9 +- template/apps/api/src/migrator/index.ts | 20 +- .../migration-log/migration-log.service.ts | 3 +- .../migration-version.service.ts | 19 +- .../apps/api/src/migrator/migrations/1.ts | 6 +- template/apps/api/src/redis-client.ts | 4 +- .../src/resources/account/account.routes.ts | 27 +- .../account/actions/forgot-password.ts | 11 +- .../api/src/resources/account/actions/get.ts | 4 +- .../src/resources/account/actions/google.ts | 85 +- .../account/actions/remove-avatar.ts | 8 +- .../resources/account/actions/resend-email.ts | 11 +- .../account/actions/reset-password.ts | 23 +- .../resources/account/actions/shadow-login.ts | 4 +- .../src/resources/account/actions/sign-in.ts | 21 +- .../src/resources/account/actions/sign-out.ts | 4 +- .../src/resources/account/actions/sign-up.ts | 28 +- .../src/resources/account/actions/update.ts | 32 +- .../account/actions/upload-avatar.ts | 15 +- .../resources/account/actions/verify-email.ts | 22 +- .../account/actions/verify-reset-token.ts | 6 +- .../apps/api/src/resources/account/index.ts | 4 +- .../apps/api/src/resources/token/index.ts | 4 +- .../api/src/resources/token/token.service.ts | 27 +- .../api/src/resources/user/actions/list.ts | 24 +- .../api/src/resources/user/actions/remove.ts | 4 +- .../api/src/resources/user/actions/update.ts | 17 +- template/apps/api/src/resources/user/index.ts | 7 +- .../resources/user/tests/user.service.spec.ts | 4 +- .../api/src/resources/user/user.handler.ts | 14 +- .../api/src/resources/user/user.routes.ts | 16 +- .../api/src/resources/user/user.service.ts | 19 +- template/apps/api/src/routes/admin.routes.ts | 6 +- template/apps/api/src/routes/index.ts | 10 +- .../middlewares/admin-auth.middleware.ts | 4 +- .../attach-custom-errors.middleware.ts | 11 +- .../middlewares/extract-tokens.middleware.ts | 2 +- .../route-error-handler.middleware.ts | 5 +- .../try-to-attach-user.middleware.ts | 8 +- .../apps/api/src/routes/private.routes.ts | 6 +- template/apps/api/src/routes/public.routes.ts | 8 +- template/apps/api/src/scheduler.ts | 11 +- template/apps/api/src/scheduler/cron/index.ts | 2 +- .../handlers/action.example.handler.ts | 4 +- .../services/analytics/analytics.service.ts | 6 +- .../apps/api/src/services/auth/auth.helper.ts | 11 +- .../api/src/services/auth/auth.service.ts | 3 +- .../cloud-storage/cloud-storage.helper.ts | 4 +- .../cloud-storage/cloud-storage.service.ts | 73 +- .../api/src/services/email/email.service.ts | 62 +- .../api/src/services/email/email.types.ts | 26 +- .../api/src/services/google/google.service.ts | 48 +- .../services/rate-limit/rate-limit.service.ts | 6 +- .../api/src/services/socket/socket.service.ts | 9 +- template/apps/api/src/types.ts | 8 +- template/apps/api/src/utils/case.util.ts | 24 + template/apps/api/src/utils/config.util.ts | 2 + template/apps/api/src/utils/index.ts | 9 +- template/apps/api/src/utils/promise.util.ts | 6 +- template/apps/api/src/utils/security.util.ts | 10 +- template/apps/api/tsconfig.json | 27 +- template/apps/web/.dockerignore | 37 +- template/apps/web/.eslintignore | 33 + template/apps/web/.eslintrc.js | 67 +- template/apps/web/.gitignore | 14 +- template/apps/web/.prettierignore | 33 + template/apps/web/.prettierrc.json | 1 + template/apps/web/.storybook/preview.tsx | 4 +- template/apps/web/next.config.js | 11 +- template/apps/web/package.json | 11 +- .../apps/web/src/components/Table/index.tsx | 113 +- .../web/src/components/Table/tbody/index.tsx | 19 +- .../web/src/components/Table/thead/index.tsx | 34 +- .../apps/web/src/pages/404/index.page.tsx | 18 +- .../Header/components/MenuToggle/index.tsx | 4 +- .../components/ShadowLoginBanner/index.tsx | 9 +- .../Header/components/UserMenu/index.tsx | 15 +- .../PageConfig/MainLayout/Header/index.tsx | 15 +- .../_app/PageConfig/MainLayout/index.tsx | 2 +- .../PageConfig/UnauthorizedLayout/index.tsx | 16 +- .../web/src/pages/_app/PageConfig/index.tsx | 12 +- template/apps/web/src/pages/_app/index.tsx | 15 +- .../apps/web/src/pages/_document/index.tsx | 3 +- .../web/src/pages/expire-token/index.page.tsx | 45 +- .../src/pages/forgot-password/index.page.tsx | 60 +- template/apps/web/src/pages/home/constants.ts | 2 +- .../apps/web/src/pages/home/index.module.css | 2 +- template/apps/web/src/pages/home/index.tsx | 51 +- .../profile/components/PhotoUpload/index.tsx | 44 +- .../apps/web/src/pages/profile/index.page.tsx | 84 +- .../src/pages/reset-password/index.page.tsx | 80 +- .../apps/web/src/pages/sign-in/index.page.tsx | 37 +- .../apps/web/src/pages/sign-up/index.page.tsx | 87 +- template/apps/web/src/query-client.ts | 2 +- .../web/src/resources/account/account.api.ts | 95 +- .../apps/web/src/resources/account/index.ts | 4 +- template/apps/web/src/resources/user/index.ts | 4 +- .../apps/web/src/resources/user/user.api.ts | 13 +- .../web/src/resources/user/user.handlers.ts | 3 +- .../web/src/services/analytics.service.ts | 3 +- template/apps/web/src/services/api.service.ts | 10 +- template/apps/web/src/services/index.ts | 8 +- template/apps/web/src/theme/index.ts | 3 +- template/apps/web/src/utils/config.util.ts | 9 +- .../apps/web/src/utils/handle-error.util.ts | 4 +- template/apps/web/src/utils/index.ts | 7 +- template/apps/web/tsconfig.json | 31 +- template/packages/app-constants/.eslintignore | 33 + template/packages/app-constants/.eslintrc.js | 21 +- template/packages/app-constants/.gitignore | 33 +- .../packages/app-constants/.prettierignore | 33 + .../packages/app-constants/.prettierrc.json | 1 + template/packages/app-constants/package.json | 13 +- template/packages/app-constants/tsconfig.json | 21 +- template/packages/app-types/.eslintignore | 33 + template/packages/app-types/.eslintrc.js | 22 +- template/packages/app-types/.gitignore | 33 +- template/packages/app-types/.prettierignore | 33 + template/packages/app-types/.prettierrc.json | 1 + template/packages/app-types/package.json | 13 +- .../packages/app-types/src/common.types.ts | 22 +- template/packages/app-types/src/index.ts | 4 +- template/packages/app-types/tsconfig.json | 21 +- template/packages/enums/.eslintignore | 33 + template/packages/enums/.eslintrc.js | 21 +- template/packages/enums/.gitignore | 33 +- template/packages/enums/.prettierignore | 33 + template/packages/enums/.prettierrc.json | 1 + template/packages/enums/package.json | 17 +- template/packages/enums/tsconfig.json | 21 +- .../packages/eslint-config-custom/.gitignore | 2 + .../eslint-config-custom/.prettierignore | 33 + .../eslint-config-custom/.prettierrc.json | 1 + .../packages/eslint-config-custom/next.js | 89 + .../packages/eslint-config-custom/node.js | 146 + .../@typescript-eslint/eslint-plugin | 1 + .../eslint-config-airbnb-typescript | 1 + .../node_modules/eslint-config-next | 1 + .../node_modules/eslint-config-prettier | 1 + .../node_modules/eslint-plugin-react | 1 + .../eslint-plugin-simple-import-sort | 1 + .../eslint-config-custom/package.json | 17 + template/packages/mailer/.eslintignore | 33 + template/packages/mailer/.eslintrc.js | 24 +- template/packages/mailer/.gitignore | 41 +- template/packages/mailer/.prettierignore | 33 + template/packages/mailer/.prettierrc.json | 1 + .../mailer/emails/_components/body-footer.tsx | 11 +- .../mailer/emails/_components/button.tsx | 7 +- .../mailer/emails/_components/head.tsx | 2 +- .../mailer/emails/_components/header.tsx | 7 +- .../mailer/emails/_components/main-footer.tsx | 7 +- template/packages/mailer/emails/_layout.tsx | 4 +- .../packages/mailer/emails/reset-password.tsx | 27 +- .../mailer/emails/sign-up-welcome.tsx | 28 +- .../packages/mailer/emails/verify-email.tsx | 30 +- template/packages/mailer/package-lock.json | 1734 ----------- template/packages/mailer/package.json | 23 +- template/packages/mailer/src/index.tsx | 12 +- template/packages/mailer/tsconfig.json | 20 +- .../packages/prettier-config-custom/index.js | 4 + .../prettier-config-custom/package.json | 5 + template/packages/schemas/.eslintignore | 33 + template/packages/schemas/.eslintrc.js | 21 +- template/packages/schemas/.gitignore | 33 +- template/packages/schemas/.prettierignore | 33 + template/packages/schemas/.prettierrc.json | 1 + template/packages/schemas/package.json | 11 +- template/packages/schemas/src/db.schema.ts | 14 +- template/packages/schemas/src/index.ts | 1 - .../packages/schemas/src/pagination.schema.ts | 8 +- template/packages/schemas/src/token.schema.ts | 17 +- template/packages/schemas/src/user.schema.ts | 36 +- template/packages/schemas/tsconfig.json | 21 +- template/packages/tsconfig/base.json | 12 + template/packages/tsconfig/nextjs.json | 24 + template/packages/tsconfig/nodejs.json | 14 + template/packages/tsconfig/package.json | 5 + template/pnpm-lock.yaml | 2603 ++++++++--------- 204 files changed, 3506 insertions(+), 4848 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 template/.vscode/settings.json create mode 100644 template/apps/api/.prettierignore create mode 100644 template/apps/api/.prettierrc.json create mode 100644 template/apps/api/src/utils/case.util.ts create mode 100644 template/apps/web/.eslintignore create mode 100644 template/apps/web/.prettierignore create mode 100644 template/apps/web/.prettierrc.json create mode 100644 template/packages/app-constants/.eslintignore create mode 100644 template/packages/app-constants/.prettierignore create mode 100644 template/packages/app-constants/.prettierrc.json create mode 100644 template/packages/app-types/.eslintignore create mode 100644 template/packages/app-types/.prettierignore create mode 100644 template/packages/app-types/.prettierrc.json create mode 100644 template/packages/enums/.eslintignore create mode 100644 template/packages/enums/.prettierignore create mode 100644 template/packages/enums/.prettierrc.json create mode 100644 template/packages/eslint-config-custom/.gitignore create mode 100644 template/packages/eslint-config-custom/.prettierignore create mode 100644 template/packages/eslint-config-custom/.prettierrc.json create mode 100644 template/packages/eslint-config-custom/next.js create mode 100644 template/packages/eslint-config-custom/node.js create mode 120000 template/packages/eslint-config-custom/node_modules/@typescript-eslint/eslint-plugin create mode 120000 template/packages/eslint-config-custom/node_modules/eslint-config-airbnb-typescript create mode 120000 template/packages/eslint-config-custom/node_modules/eslint-config-next create mode 120000 template/packages/eslint-config-custom/node_modules/eslint-config-prettier create mode 120000 template/packages/eslint-config-custom/node_modules/eslint-plugin-react create mode 120000 template/packages/eslint-config-custom/node_modules/eslint-plugin-simple-import-sort create mode 100644 template/packages/eslint-config-custom/package.json create mode 100644 template/packages/mailer/.eslintignore create mode 100644 template/packages/mailer/.prettierignore create mode 100644 template/packages/mailer/.prettierrc.json delete mode 100644 template/packages/mailer/package-lock.json create mode 100644 template/packages/prettier-config-custom/index.js create mode 100644 template/packages/prettier-config-custom/package.json create mode 100644 template/packages/schemas/.eslintignore create mode 100644 template/packages/schemas/.prettierignore create mode 100644 template/packages/schemas/.prettierrc.json create mode 100644 template/packages/tsconfig/base.json create mode 100644 template/packages/tsconfig/nextjs.json create mode 100644 template/packages/tsconfig/nodejs.json create mode 100644 template/packages/tsconfig/package.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f8ef33c6..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eslint.workingDirectories": [ - { "mode": "auto" } - ] -} diff --git a/deploy/aws/deploy/script/Dockerfile b/deploy/aws/deploy/script/Dockerfile index fecfef27..dba4e33a 100644 --- a/deploy/aws/deploy/script/Dockerfile +++ b/deploy/aws/deploy/script/Dockerfile @@ -44,4 +44,4 @@ RUN npm i --prefix ./deploy/script --progress=false --no-audit --production ADD ./ ./ -CMD node ./deploy/script/src/index.js +CMD node ./deploy/script/src/node.js diff --git a/deploy/digital-ocean/deploy/script/Dockerfile b/deploy/digital-ocean/deploy/script/Dockerfile index c39832a5..1f4baf45 100644 --- a/deploy/digital-ocean/deploy/script/Dockerfile +++ b/deploy/digital-ocean/deploy/script/Dockerfile @@ -37,4 +37,4 @@ RUN npm i --prefix ./deploy/script --progress=false --no-audit --production ADD ./ ./ -CMD node ./deploy/script/src/index.js +CMD node ./deploy/script/src/node.js diff --git a/docs/deployment/kubernetes/overview.mdx b/docs/deployment/kubernetes/overview.mdx index 4994a698..53d6e330 100644 --- a/docs/deployment/kubernetes/overview.mdx +++ b/docs/deployment/kubernetes/overview.mdx @@ -45,7 +45,7 @@ We use [**Blue-Green**](https://martinfowler.com/bliki/BlueGreenDeployment.html) This is the main part of the deployment script. -```javascript deploy/script/src/index.js +```javascript deploy/script/src/node.js const buildAndPushImage = async ({ dockerFilePath, dockerRepo, dockerContextDir, imageTag, environment }) => { await execCommand(`docker build \ --build-arg APP_ENV=${environment} \ @@ -156,7 +156,7 @@ Services are parts of your application packaged as Helm Charts. |[**Scheduler**](https://github.com/paralect/ship/blob/main/template/apps/api/src/scheduler)|Service that runs cron jobs|Pod| |[**Migrator**](https://github.com/paralect/ship/blob/main/template/apps/api/src/migrator)|Service that migrates database schema. It deploys before api through Helm pre-upgrade [hook](https://helm.sh/docs/topics/charts_hooks/)|Job| -To deploy services in the cluster manually you need to set cluster authorization credentials inside [config](https://github.com/paralect/ship/blob/main/examples/base/deploy/script/src/config.js) and run deployment [script](https://github.com/paralect/ship/blob/main/examples/base/deploy/script/src/index.js). +To deploy services in the cluster manually you need to set cluster authorization credentials inside [config](https://github.com/paralect/ship/blob/main/examples/base/deploy/script/src/config.js) and run deployment [script](https://github.com/paralect/ship/blob/main/examples/base/deploy/script/src/node.js). ```shell deploy/script/src node index diff --git a/docs/web/styling.mdx b/docs/web/styling.mdx index e790ddd8..001c53cd 100644 --- a/docs/web/styling.mdx +++ b/docs/web/styling.mdx @@ -10,7 +10,7 @@ Mantine UI offers a wide range of customizable components and hooks that facilit Our application's design system is built on top of Mantine's [theme object](https://v6.mantine.dev/theming/theme-object/). The theme object allows us to define global style properties that can be applied consistently across web application. -### Custom Components (`components/index.js`) +### Custom Components (`components/node.js`) We extend Mantine's components to create custom-styled versions specific to our application needs. diff --git a/template/.dockerignore b/template/.dockerignore index f5425e1a..7422e1e9 100644 --- a/template/.dockerignore +++ b/template/.dockerignore @@ -1,2 +1,27 @@ -node_modules -*/**/node_modules +# dependencies +/node_modules + +# testing +/coverage + +# production +/dist + +# edtiors +.idea +.vscode + +# misc +.DS_Store +.dev +.turbo +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo diff --git a/template/.gitignore b/template/.gitignore index bbd791b2..7422e1e9 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -1,7 +1,27 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/dist + +# edtiors .idea -.turbo +.vscode + +# misc .DS_Store .dev -/node_modules -dist -coverage +.turbo +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo diff --git a/template/.vscode/settings.json b/template/.vscode/settings.json deleted file mode 100644 index f8ef33c6..00000000 --- a/template/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eslint.workingDirectories": [ - { "mode": "auto" } - ] -} diff --git a/template/apps/api/.dockerignore b/template/apps/api/.dockerignore index 3c3629e6..924a89e0 100644 --- a/template/apps/api/.dockerignore +++ b/template/apps/api/.dockerignore @@ -1 +1,23 @@ -node_modules +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo diff --git a/template/apps/api/.eslintignore b/template/apps/api/.eslintignore index 1c829f4e..924a89e0 100644 --- a/template/apps/api/.eslintignore +++ b/template/apps/api/.eslintignore @@ -1,2 +1,23 @@ -node_modules/* -jest.config.ts +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo diff --git a/template/apps/api/.eslintrc.js b/template/apps/api/.eslintrc.js index 7c79cac5..0ce0e0dc 100644 --- a/template/apps/api/.eslintrc.js +++ b/template/apps/api/.eslintrc.js @@ -1,61 +1,4 @@ -const { resolve } = require("path"); - module.exports = { root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "import", "tsc"], - extends: [ - "airbnb-typescript/base", - "plugin:@typescript-eslint/recommended", - ], - env: { - node: true, - }, - parserOptions: { - ecmaVersion: 13, - sourceType: "module", - project: "./tsconfig.json", - // use tsconfig relative to eslintrc file for IDE - tsconfigRootDir: __dirname, - }, - rules: { - 'tsc/config': [2, { - configFile: resolve(__dirname, './tsconfig.json') - }], - 'arrow-body-style': 0, - 'no-underscore-dangle': 0, - 'function-paren-newline': 1, - 'import/no-extraneous-dependencies': ['error', { - devDependencies: [ - '**/*.spec.{js,ts}', - '**/*.builder.{js,ts}', - ], - }], - 'max-len': ['warn', { - code: 120, - ignoreStrings: true, - ignoreUrls: true, - ignoreTemplateLiterals: true, - }], - }, - settings: { - "import/resolver": { - "node": { - "moduleDirectory": [ - "src", - "node_modules" - ] - } - } - }, - ignorePatterns: [ - // ignore this file - ".eslintrc.js", - // never lint node modules - "node_modules", - // ignore prod_node_modules copied in Docker - "prod_node_modules", - // ignore output build files - "dist" - ], + extends: ['custom/node'], }; diff --git a/template/apps/api/.gitignore b/template/apps/api/.gitignore index a4005fdc..f9d6ffbf 100644 --- a/template/apps/api/.gitignore +++ b/template/apps/api/.gitignore @@ -1,10 +1,28 @@ -node_modules +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/dist + +# misc .DS_Store -npm-debug.log -.idea -.vscode -./dist +*.pem +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files .env .env.* !.env.example + +# typescript +*.tsbuildinfo diff --git a/template/apps/api/.prettierignore b/template/apps/api/.prettierignore new file mode 100644 index 00000000..924a89e0 --- /dev/null +++ b/template/apps/api/.prettierignore @@ -0,0 +1,23 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo diff --git a/template/apps/api/.prettierrc.json b/template/apps/api/.prettierrc.json new file mode 100644 index 00000000..ba63a7c6 --- /dev/null +++ b/template/apps/api/.prettierrc.json @@ -0,0 +1 @@ +"prettier-config-custom" diff --git a/template/apps/api/README.md b/template/apps/api/README.md index 29522bea..9f825af7 100644 --- a/template/apps/api/README.md +++ b/template/apps/api/README.md @@ -26,14 +26,12 @@ docker-compose up --build To start the project not in the docker container just run: `npm run dev`. This command will start the application on port `3001` and will automatically restart whenever you change any file in `./src` directory. - ### Important notes -You need to set ```APP_ENV``` variable in build args in a place where you deploy application. It is responsible for the config file from ```environment``` folder that will be taken when building your application - +You need to set `APP_ENV` variable in build args in a place where you deploy application. It is responsible for the config file from `environment` folder that will be taken when building your application -| APP_ENV | File | -| ------------- | ------------- | -| development | development.json | -| staging | staging.json | -| production | production.json | +| APP_ENV | File | +| ----------- | ---------------- | +| development | development.json | +| staging | staging.json | +| production | production.json | diff --git a/template/apps/api/jest.config.ts b/template/apps/api/jest.config.ts index bf0bf724..acfbb53b 100644 --- a/template/apps/api/jest.config.ts +++ b/template/apps/api/jest.config.ts @@ -4,9 +4,7 @@ const config: JestConfigWithTsJest = { preset: '@shelf/jest-mongodb', verbose: true, testEnvironment: 'node', - testMatch: [ - '**/?(*.)+(spec.ts)', - ], + testMatch: ['**/?(*.)+(spec.ts)'], transform: { '^.+\\.(ts)$': 'ts-jest', }, diff --git a/template/apps/api/package.json b/template/apps/api/package.json index 4ec7a209..08310d2a 100644 --- a/template/apps/api/package.json +++ b/template/apps/api/package.json @@ -18,9 +18,9 @@ "precommit": "lint-staged" }, "dependencies": { - "@aws-sdk/client-s3": "3.289.0", - "@aws-sdk/lib-storage": "3.289.0", - "@aws-sdk/s3-request-presigner": "3.289.0", + "@aws-sdk/client-s3": "3.540.0", + "@aws-sdk/lib-storage": "3.540.0", + "@aws-sdk/s3-request-presigner": "3.540.0", "@koa/cors": "4.0.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.0", @@ -75,22 +75,19 @@ "@types/node": "20.11.1", "@types/node-schedule": "2.1.0", "@types/psl": "1.1.0", - "@typescript-eslint/eslint-plugin": "5.54.1", - "@typescript-eslint/parser": "5.54.1", - "eslint": "8.36.0", - "eslint-config-airbnb-base": "15.0.0", - "eslint-config-airbnb-typescript": "17.0.0", - "eslint-plugin-import": "2.27.5", - "eslint-plugin-tsc": "2.0.0", + "eslint": "8.56.0", + "eslint-config-custom": "workspace:*", "jest": "29.5.0", "lint-staged": "13.2.0", "mongodb-memory-server": "8.12.0", - "nodemon": "3.0.1", "npm-run-all": "4.1.5", + "prettier": "3.2.5", + "prettier-config-custom": "workspace:*", "ts-jest": "29.0.5", "ts-node": "10.9.1", "ts-node-dev": "2.0.0", - "typescript": "4.9.5" + "tsconfig": "workspace:*", + "typescript": "5.3.3" }, "lint-staged": { "*.ts": [ diff --git a/template/apps/api/src/app.ts b/template/apps/api/src/app.ts index 42b0cd85..57296187 100644 --- a/template/apps/api/src/app.ts +++ b/template/apps/api/src/app.ts @@ -1,29 +1,28 @@ // allows to require modules relative to /src folder // for example: require('lib/mongo/idGenerator') // all options can be found here: https://gist.github.com/branneman/8048520 -import moduleAlias from 'module-alias'; -moduleAlias.addPath(__dirname); -moduleAlias(); // read aliases from package json - -import 'dotenv/config'; - -import http from 'http'; import cors from '@koa/cors'; +import http from 'http'; +import ioEmitter from 'io-emitter'; import bodyParser from 'koa-bodyparser'; import helmet from 'koa-helmet'; -import qs from 'koa-qs'; import koaLogger from 'koa-logger'; - -import { AppKoa } from 'types'; +import qs from 'koa-qs'; +import logger from 'logger'; +import moduleAlias from 'module-alias'; // read aliases from package json +import redisClient, { redisErrorHandler } from 'redis-client'; import { socketService } from 'services'; +import routes from 'routes'; import config from 'config'; -import logger from 'logger'; -import routes from 'routes'; -import redisClient, { redisErrorHandler } from 'redis-client'; -import ioEmitter from 'io-emitter'; +import { AppKoa } from 'types'; + +import 'dotenv/config'; + +moduleAlias.addPath(__dirname); +moduleAlias(); const initKoa = () => { const app = new AppKoa(); @@ -31,21 +30,25 @@ const initKoa = () => { app.use(cors({ credentials: true })); app.use(helmet()); qs(app); - app.use(bodyParser({ - enableTypes: ['json', 'form', 'text'], - onerror: (err: Error, ctx) => { - const errText: string = err.stack || err.toString(); - logger.warn(`Unable to parse request body. ${errText}`); - ctx.throw(422, 'Unable to parse request JSON.'); - }, - })); - app.use(koaLogger({ - transporter: (message, args) => { - const [, method, endpoint, status, time, length] = args; - - logger.http(message.trim(), { method, endpoint, status, time, length }); - }, - })); + app.use( + bodyParser({ + enableTypes: ['json', 'form', 'text'], + onerror: (err: Error, ctx) => { + const errText: string = err.stack || err.toString(); + logger.warn(`Unable to parse request body. ${errText}`); + ctx.throw(422, 'Unable to parse request JSON.'); + }, + }), + ); + app.use( + koaLogger({ + transporter: (message, args) => { + const [, method, endpoint, status, time, length] = args; + + logger.http(message.trim(), { method, endpoint, status, time, length }); + }, + }), + ); routes(app); @@ -58,10 +61,13 @@ const app = initKoa(); const server = http.createServer(app.callback()); if (config.REDIS_URI) { - await redisClient.connect().then(() => { - ioEmitter.initClient(); - socketService(server); - }).catch(redisErrorHandler); + await redisClient + .connect() + .then(() => { + ioEmitter.initClient(); + socketService(server); + }) + .catch(redisErrorHandler); } server.listen(config.PORT, () => { diff --git a/template/apps/api/src/config/retool/users-management.json b/template/apps/api/src/config/retool/users-management.json index a7eb355c..4e098ebe 100644 --- a/template/apps/api/src/config/retool/users-management.json +++ b/template/apps/api/src/config/retool/users-management.json @@ -1,2 +1,167 @@ -{"uuid":"d0f48596-326b-11ed-b65c-47fd8dd25b08","page":{"id":93448628,"data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"shadowLogin\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"shadowLogin\",\"type\",\"datasource\",\"subtype\",\"RESTQuery\",\"namespace\",null,\"resourceName\",\"bb8ab17c-b112-4444-b054-3067ca2b62dc\",\"resourceDisplayName\",\"shadowLogin\",\"template\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"[{\\\"key\\\":\\\"id\\\",\\\"value\\\":\\\"\\\\\\\"6315f482ec588c3bcb5b07a8\\\\\\\"\\\"}]\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/account/shadow-login\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"~#iL\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",\"\",\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"POST\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"style\",null,\"position2\",null,\"mobilePosition2\",null,\"mobileAppPosition\",null,\"tabIndex\",null,\"container\",\"\",\"createdAt\",\"~m1662967609500\",\"updatedAt\",\"~m1662977816251\",\"folder\",\"apiUser\",\"screen\",null]]],\"list\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"list\",\"^4\",\"datasource\",\"^5\",\"NoSqlQuery\",\"^6\",null,\"^7\",\"a968b944-81f3-45be-9acc-e92dd5356d4a\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"method\",\"find\",\"lastReceivedFromResourceAt\",null,\"aggregation\",\"\",\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",true,\"showFailureToaster\",true,\"query\",\"{{searchQuery.value}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"update\",\"\",\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"useRawCollectionName\",false,\"data\",null,\"operations\",\"\",\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"projection\",\"{passwordHash: 0 }\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"sortBy\",\"\",\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[]],\"insert\",\"\",\"queryTimeout\",\"10000\",\"field\",\"\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"database\",\"\",\"changesetIsObject\",false,\"limit\",\"\",\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"options\",\"\",\"collection\",\"users\",\"skip\",\"\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653827447242\",\"^B\",\"~m1653842042013\",\"^C\",\"dbUser\",\"^D\",null]]],\"searchQuery\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"searchQuery\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"funcBody\",\"\\nconst escapeRegExp = (string) => {\\n return string.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g, '\\\\\\\\$&'); // $& means the whole matched string\\n}\\n\\nconst regexQuery = {\\n $regex: `.*${escapeRegExp({{search_bar.value}})}.*`,\\n $options:'i'\\n}\\n\\nreturn {\\n $or: [\\n { _id: regexQuery },\\n { firstName: regexQuery },\\n { lastName: regexQuery },\\n { email: regexQuery },\\n ],\\n}\\n\\n\",\"value\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653831767909\",\"^B\",\"~m1653841921344\",\"^C\",\"dbUser\",\"^D\",null]]],\"update\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"update\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"[{\\\"key\\\":\\\"firstName\\\",\\\"value\\\":\\\"{{firstNameInput.value}}\\\"},{\\\"key\\\":\\\"lastName\\\",\\\"value\\\":\\\"{{lastNameInput.value}}\\\"},{\\\"key\\\":\\\"email\\\",\\\"value\\\":\\\"{{emailInput.value}}\\\"}]\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/users/{{usersTable.selectedRow.data._id}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"userUpdateModal.close();\\nlist.trigger();\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"PUT\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653836736485\",\"^B\",\"~m1654498069760\",\"^C\",\"apiUser\",\"^D\",null]]],\"remove\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"remove\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/users/{{usersTable.selectedRow.data._id}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"DELETE\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653840309251\",\"^B\",\"~m1654498060079\",\"^C\",\"apiUser\",\"^D\",null]]],\"verifyEmail\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"verifyEmail\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"account/verify-email/?token={{usersTable.selectedRow.data.signupToken}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"failure\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"GET\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653838337897\",\"^B\",\"~m1653842008069\",\"^C\",\"apiUser\",\"^D\",null]]],\"welcomeText\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"welcomeText\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"bottom\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### Users Management\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",null,\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"header\",\"row\",0,\"col\",0,\"height\",0.6,\"width\",4,\"tabNum\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653827091457\",\"^B\",\"~m1653834455274\",\"^C\",\"\",\"^D\",null]]],\"usersTable\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"usersTable\",\"^4\",\"widget\",\"^5\",\"TableWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"showCustomButton\",true,\"sortMappedValue\",[\"^3\",[]],\"_filteredSortedRenderedDataWithTypes\",null,\"heightType\",\"fixed\",\"normalizedData\",null,\"rowHeight\",\"standard\",\"saveChangesDisabled\",\"\",\"columnTypeProperties\",[\"^3\",[]],\"columnWidths\",[\"^:\",[]],\"showSummaryFooter\",false,\"disableRowSelectInteraction\",false,\"columnWidthsMobile\",[\"^:\",[]],\"hasNextAfterCursor\",\"\",\"columnTypeSpecificExtras\",[\"^3\",[]],\"onRowAdded\",\"\",\"columnHeaderNames\",[\"^3\",[]],\"alwaysShowPaginator\",false,\"columnColors\",[\"^3\",[\"lastName\",\"\",\"signupToken\",\"\",\"createdOn\",\"\",\"passwordHash\",\"\",\"deletedOn\",\"\",\"lastRequest\",\"\",\"isEmailVerified\",\"\",\"_id\",\"\",\"updatedOn\",\"\",\"firstName\",\"\",\"email\",\"\"]],\"columnFrozenAlignments\",[\"^3\",[]],\"allowMultiRowSelect\",false,\"columnFormats\",[\"^3\",[]],\"columnRestrictedEditing\",[\"^3\",[]],\"showFilterButton\",true,\"_columnVisibility\",[\"^3\",[\"lastRequest\",true,\"signupToken\",false]],\"_columnSummaryTypes\",[\"^3\",[]],\"_columnsWithLegacyBackgroundColor\",[\"~#iOS\",[]],\"showAddRowButton\",false,\"_unfilteredSelectedIndex\",null,\"nextBeforeCursor\",\"\",\"columnVisibility\",[\"^3\",[]],\"selectedPageIndex\",\"0\",\"applyDynamicSettingsToColumnOrder\",true,\"rowColor\",[],\"actionButtonColumnName\",\"Actions\",\"resetAfterSave\",true,\"filterStackType\",\"and\",\"downloadRawData\",false,\"showFetchingIndicator\",true,\"serverPaginated\",false,\"data\",\"{{list.data}}\",\"displayedData\",null,\"actionButtons\",[\"^:\",[]],\"actionButtonSelectsRow\",true,\"selectRowByDefault\",true,\"defaultSortByColumn\",\"\",\"paginationOffset\",0,\"columnAlignment\",[\"^3\",[]],\"columnSummaries\",[\"^ \"],\"showBoxShadow\",true,\"sortedDesc\",false,\"customButtonName\",\"\",\"columnMappersRenderAsHTML\",[\"^3\",[]],\"showRefreshButton\",true,\"pageSize\",\"20\",\"useDynamicColumnSettings\",false,\"actionButtonPosition\",\"left\",\"dynamicRowHeights\",false,\"bulkUpdateAction\",\"\",\"afterCursor\",\"\",\"onCustomButtonPressQueryName\",\"\",\"changeSet\",[\"^ \"],\"sortedColumn\",\"\",\"_columnSummaryValues\",[\"^3\",[]],\"checkboxRowSelect\",true,\"_compatibilityMode\",false,\"showColumnBorders\",false,\"clearSelectionLabel\",\"Clear selection\",\"_renderedDataWithTypes\",null,\"columnAllowOverflow\",[\"^3\",[]],\"beforeCursor\",\"\",\"serverPaginationType\",\"limitOffsetBased\",\"onRowSelect\",\"\",\"showDownloadButton\",true,\"selectedIndex\",null,\"defaultSortDescending\",false,\"_sortedDisplayedDataIndices\",null,\"dynamicColumnSettings\",null,\"totalRowCount\",\"\",\"recordUpdates\",[],\"newRow\",null,\"emptyMessage\",\"No rows found\",\"columnEditable\",[\"^3\",[\"_id\",false,\"firstName\",false,\"lastRequest\",false,\"updatedOn\",false]],\"_viewerColumnSummaryTypes\",[\"^ \"],\"filters\",[],\"displayedDataIndices\",null,\"disableSorting\",[\"^3\",[]],\"columnMappers\",[\"^3\",[]],\"showClearSelection\",false,\"doubleClickToEdit\",true,\"overflowType\",\"pagination\",\"_reverseSortedDisplayedDataIndices\",null,\"showTableBorder\",true,\"selectedCell\",[\"^ \",\"index\",null,\"data\",null,\"columnName\",null],\"columns\",[\"^:\",[]],\"defaultSelectedRow\",\"first\",\"freezeActionButtonColumns\",false,\"sort\",null,\"_columns\",[\"^:\",[]],\"sortByRawValue\",[\"^3\",[]],\"calculatedColumns\",[\"^:\",[]],\"selectedRow\",[\"^ \",\"^K\",null,\"^L\",null],\"showPaginationOnTop\",false,\"_reverseDisplayedDataIndices\",null,\"nextAfterCursor\",\"\",\"useCompactMode\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",2.3999999999999995,\"col\",0,\"^G\",10.399999999999999,\"^H\",8,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653828995649\",\"^B\",\"~m1654498035519\",\"^C\",\"\",\"^D\",null]]],\"search_bar\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"search_bar\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Search by name, email or _id\",\"label\",\"\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",0.7999999999999999,\"col\",0,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653830224018\",\"^B\",\"~m1653830294498\",\"^C\",\"\",\"^D\",null]]],\"container1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"container1\",\"^4\",\"widget\",\"^5\",\"ContainerWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"_disabledByIndex\",[\"^:\",[\"\"]],\"heightType\",\"auto\",\"currentViewKey\",null,\"iconByIndex\",[],\"clickable\",false,\"_iconByIndex\",[\"^:\",[\"\"]],\"hidden\",\"\",\"showHeader\",true,\"hoistFetching\",true,\"views\",[],\"showInEditor\",false,\"tooltipText\",\"\",\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^:\",[\"\"]],\"currentViewIndex\",null,\"_hasMigratedNestedItems\",true,\"transition\",\"none\",\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^:\",[\"\"]],\"tooltipByIndex\",[],\"showFooter\",false,\"_viewKeys\",[\"^:\",[\"View 1\"]],\"events\",[\"^3\",[]],\"_ids\",[\"^:\",[\"6af98\"]],\"viewKeys\",[],\"iconPositionByIndex\",[],\"_iconPositionByIndex\",[\"^:\",[\"\"]],\"loading\",false,\"overflowType\",\"scroll\",\"disabled\",false,\"_labels\",[\"^:\",[\"\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"showBody\",true,\"labels\",[]]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",2.400000000000001,\"col\",8,\"^G\",10.4,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745012\",\"^B\",\"~m1653833745012\",\"^C\",\"\",\"^D\",null]]],\"containerTitle1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"containerTitle1\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### {{usersTable.selectedRow.data.firstName}} {{usersTable.selectedRow.data.lastName}} \",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"header\",\"^F\",\"\",\"row\",0.20000000000000012,\"col\",0,\"^G\",0.8,\"^H\",8,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745583\",\"^B\",\"~m1653838314223\",\"^C\",\"\",\"^D\",null]]],\"keyValue1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"keyValue1\",\"^4\",\"widget\",\"^5\",\"KeyValueMapWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"rowHeaderNames\",[\"^3\",[]],\"rowFormats\",[\"^3\",[]],\"valueTitle\",\"Value\",\"data\",\"{{usersTable.selectedRow.data}}\",\"prevRowMappers\",[\"^3\",[]],\"rowMappersRenderAsHTML\",[\"^3\",[]],\"rowVisibility\",[\"^3\",[\"a\",true,\"lastName\",true,\"signupToken\",true,\"b\",true,\"c\",true,\"createdOn\",true,\"deletedOn\",true,\"lastRequest\",true,\"isEmailVerified\",true,\"_id\",true,\"updatedOn\",true,\"firstName\",true,\"email\",true]],\"prevRowFormats\",[\"^3\",[]],\"rowMappers\",[\"^3\",[]],\"rows\",[\"^:\",[\"a\",\"b\",\"c\",\"_id\",\"firstName\",\"lastName\",\"email\",\"isEmailVerified\",\"createdOn\",\"updatedOn\",\"lastRequest\",\"signupToken\",\"deletedOn\"]],\"keyTitle\",\"Key\"]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"body\",\"^F\",\"6af98\",\"row\",0,\"col\",0,\"^G\",8.4,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745869\",\"^B\",\"~m1654498035544\",\"^C\",\"\",\"^D\",null]]],\"userActions\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"userActions\",\"^4\",\"widget\",\"^5\",\"DropdownButtonWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"_disabledByIndex\",[\"^:\",[\"\",\"{{usersTable.selectedRow.data.isEmailVerified}}\",\"\",false]],\"horizontalAlign\",\"stretch\",\"_values\",[\"^:\",[\"\",\"\",\"\",\"Action 4\"]],\"iconByIndex\",[],\"iconPosition\",\"left\",\"clickable\",false,\"_iconByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"hidden\",false,\"data\",[],\"text\",\"Menu\",\"_fallbackTextByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"showInEditor\",false,\"tooltipText\",\"\",\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^:\",[\"\",\"\",\"\",false]],\"_captionByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"styleVariant\",\"outline\",\"_hasMigratedNestedItems\",true,\"captionByIndex\",[],\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"_colorByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"tooltipByIndex\",[],\"icon\",\"\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"shadowLogin\",\"targetId\",\"1faf9\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"remove\",\"targetId\",\"d452d\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"verifyEmail\",\"targetId\",\"ccb37\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"widget\",\"method\",\"open\",\"pluginId\",\"userUpdateModal\",\"targetId\",\"59b6e\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"_ids\",[\"^:\",[\"59b6e\",\"ccb37\",\"d452d\",\"1faf9\"]],\"overlayMaxHeight\",375,\"disabled\",false,\"_labels\",[\"^:\",[\"Update\",\"Verify Email\",\"Remove\",\"Shadow login\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"_imageByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"labels\",[]]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"header\",\"^F\",\"\",\"row\",0.2,\"col\",9,\"^G\",0.8,\"^H\",3,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833840942\",\"^B\",\"~m1662970504941\",\"^C\",\"\",\"^D\",null]]],\"userUpdateModal\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"userUpdateModal\",\"^4\",\"widget\",\"^5\",\"ModalWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"opened\",false,\"modalOverflowType\",\"scroll\",\"hidden\",\"true\",\"onModalClose\",\"\",\"modalHeightType\",\"fixed\",\"tooltipText\",\"\",\"modalHeight\",\"\",\"onModalOpen\",\"\",\"modalWidth\",\"\",\"closeOnOutsideClick\",true,\"loading\",\"\",\"disabled\",\"\",\"buttonText\",\"Open Modal\"]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",13.399999999999999,\"col\",8,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834649657\",\"^B\",\"~m1653834683865\",\"^C\",\"\",\"^D\",null]]],\"updateUserForm\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"updateUserForm\",\"^4\",\"widget\",\"^5\",\"FormWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"disableSubmit\",false,\"heightType\",\"auto\",\"resetAfterSubmit\",true,\"submitting\",false,\"hidden\",false,\"data\",[\"^ \"],\"showHeader\",true,\"hoistFetching\",true,\"initialData\",null,\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"border\",\"\"]],\"invalid\",false,\"showFooter\",true,\"events\",[\"^:\",[[\"^3\",[\"event\",\"submit\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"update\",\"targetId\",null,\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"overflowType\",\"scroll\",\"disabled\",false,\"requireValidation\",true,\"maintainSpaceWhenHidden\",false,\"showBody\",true]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"userUpdateModal\",\"^E\",\"body\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",0.2,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834910846\",\"^B\",\"~m1653841961694\",\"^C\",\"\",\"^D\",null]]],\"formTitle1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"formTitle1\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### Update {{ usersTable.selectedRow.data.firstName}} {{usersTable.selectedRow.data.lastName}}\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"header\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",0.6,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834911218\",\"^B\",\"~m1653838314310\",\"^C\",\"\",\"^D\",null]]],\"formButton1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"formButton1\",\"^4\",\"widget\",\"^5\",\"ButtonWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"horizontalAlign\",\"stretch\",\"clickable\",false,\"iconAfter\",\"\",\"submitTargetId\",\"updateUserForm\",\"hidden\",false,\"text\",\"Submit\",\"showInEditor\",false,\"tooltipText\",\"\",\"submit\",true,\"iconBefore\",\"\",\"events\",[\"^3\",[]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"footer\",\"^F\",\"\",\"row\",0.40000000000000036,\"col\",8,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834911337\",\"^B\",\"~m1653835572234\",\"^C\",\"\",\"^D\",null]]],\"firstNameInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"firstNameInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{usersTable.selectedRow.data.firstName}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Enter value\",\"label\",\"First Name\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653835222740\",\"^B\",\"~m1653838314340\",\"^C\",\"\",\"^D\",null]]],\"lastNameInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"lastNameInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\" {{usersTable.selectedRow.data.lastName}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Enter value\",\"label\",\"Last Name\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",1,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653835244433\",\"^B\",\"~m1653838314371\",\"^C\",\"\",\"^D\",null]]],\"emailInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"emailInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"email\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{usersTable.selectedRow.data.email}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"you@example.com\",\"label\",\"Email\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"bold/mail-send-envelope\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",2,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653836295252\",\"^B\",\"~m1653838314403\",\"^C\",\"\",\"^D\",null]]],\"$main\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$main\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"type\",\"main\",\"sticky\",false]],\"^;\",[\"^3\",[]],\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1662967417421\",\"^B\",\"~m1662967417421\",\"^C\",\"\",\"^D\",null]]],\"$header\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$header\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"type\",\"header\",\"sticky\",true]],\"^;\",[\"^3\",[]],\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1662967417421\",\"^B\",\"~m1662967417421\",\"^C\",\"\",\"^D\",null]]]]],\"^A\",null,\"version\",\"2.99.1\",\"appThemeId\",null,\"preloadedAppJavaScript\",null,\"preloadedAppJSLinks\",[],\"testEntities\",[],\"tests\",[],\"appStyles\",\"\",\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^:\",[]],\"pageLoadValueOverrides\",[\"^:\",[]],\"customDocumentTitle\",\"\",\"customDocumentTitleEnabled\",false,\"customShortcuts\",[],\"isGlobalWidget\",false,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"folders\",[\"^:\",[\"dbUser\",\"apiUser\"]],\"queryStatusVisibility\",true,\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null,\"instrumentationEnabled\",false,\"experimentalPerfFeatures\",[\"^ \",\"batchCommitModelEnabled\",false,\"skipDepCycleCheckingEnabled\",false,\"serverDepGraphEnabled\",false,\"useRuntimeV2\",false],\"experimentalDataTabEnabled\",false]]]"},"changesRecord":[{"type":"DATASOURCE_TYPE_CHANGE","payload":{"newType":"RESTQuery","pluginId":"shadowLogin","resourceName":"bb8ab17c-b112-4444-b054-3067ca2b62dc"}},{"type":"WIDGET_TEMPLATE_UPDATE","payload":{"plugin":{"id":"shadowLogin","type":"datasource","style":null,"folder":"apiUser","screen":null,"subtype":"RESTQuery","tabIndex":null,"template":{"body":"","data":null,"type":"GET","error":null,"query":"","events":[],"cookies":"","headers":"","rawData":null,"bodyType":"json","metadata":null,"changeset":"","timestamp":0,"isFetching":false,"isImported":false,"cacheKeyTtl":"","transformer":"// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data","queryTimeout":"10000","allowedGroups":[],"enableCaching":false,"privateParams":[],"queryDisabled":"","watchedParams":[],"successMessage":"","changesetObject":"","paginationLimit":"","errorTransformer":"// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error","queryRefreshTime":"","runWhenPageLoads":false,"changesetIsObject":false,"enableTransformer":false,"paginationEnabled":false,"queryThrottleTime":"750","queryTriggerDelay":"0","showFailureToaster":true,"showSuccessToaster":true,"importedQueryInputs":{},"paginationDataField":"","playgroundQueryUuid":"","requireConfirmation":false,"runWhenModelUpdates":true,"notificationDuration":"","queryDisabledMessage":"","resourceNameOverride":"","importedQueryDefaults":{},"playgroundQuerySaveId":"latest","runWhenPageLoadsDelay":"","enableErrorTransformer":false,"queryFailureConditions":"","paginationPaginationField":"","updateSetValueDynamically":false,"showLatestVersionUpdatedWarning":false,"showUpdateSetValueDynamicallyToggle":true},"container":"","createdAt":"2022-09-12T07:26:49.500Z","namespace":null,"position2":null,"updatedAt":"2022-09-12T10:14:09.072Z","resourceName":"bb8ab17c-b112-4444-b054-3067ca2b62dc","mobilePosition2":null,"mobileAppPosition":null,"resourceDisplayName":null},"update":{"body":"[{\"key\":\"id\",\"value\":\"\\\"6315f482ec588c3bcb5b07a8\\\"\"}]","data":null,"type":"POST","error":null,"query":"admin/account/shadow-login","events":[],"cookies":"","headers":"","rawData":null,"bodyType":"json","metadata":null,"changeset":"","timestamp":0,"isFetching":false,"isImported":false,"cacheKeyTtl":"","transformer":"// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data","queryTimeout":"10000","allowedGroups":[],"enableCaching":false,"privateParams":[],"queryDisabled":"","watchedParams":[],"successMessage":"","changesetObject":"","paginationLimit":"","errorTransformer":"// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error","queryRefreshTime":"","runWhenPageLoads":false,"changesetIsObject":false,"enableTransformer":false,"paginationEnabled":false,"playgroundQueryId":null,"queryThrottleTime":"750","queryTriggerDelay":"0","showFailureToaster":true,"showSuccessToaster":true,"confirmationMessage":null,"importedQueryInputs":{},"paginationDataField":"","playgroundQueryUuid":"","requireConfirmation":false,"runWhenModelUpdates":false,"notificationDuration":"","queryDisabledMessage":"","resourceNameOverride":"","resourceTypeOverride":"","importedQueryDefaults":{},"playgroundQuerySaveId":"latest","runWhenPageLoadsDelay":"","enableErrorTransformer":false,"queryFailureConditions":"","paginationPaginationField":"","updateSetValueDynamically":false,"lastReceivedFromResourceAt":null,"showLatestVersionUpdatedWarning":false,"showUpdateSetValueDynamicallyToggle":true},"widgetId":"shadowLogin","shouldRecalculateTemplate":true},"isUserTriggered":true}],"gitSha":null,"checksum":null,"createdAt":"2022-09-12T10:16:56.671Z","updatedAt":"2022-09-12T10:16:56.671Z","pageId":1427949,"userId":403318,"branchId":null},"modules":{}} - +{ + "uuid": "d0f48596-326b-11ed-b65c-47fd8dd25b08", + "page": { + "id": 93448628, + "data": { + "appState": "[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"shadowLogin\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"shadowLogin\",\"type\",\"datasource\",\"subtype\",\"RESTQuery\",\"namespace\",null,\"resourceName\",\"bb8ab17c-b112-4444-b054-3067ca2b62dc\",\"resourceDisplayName\",\"shadowLogin\",\"template\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"[{\\\"key\\\":\\\"id\\\",\\\"value\\\":\\\"\\\\\\\"6315f482ec588c3bcb5b07a8\\\\\\\"\\\"}]\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/account/shadow-login\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"~#iL\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",\"\",\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"POST\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"style\",null,\"position2\",null,\"mobilePosition2\",null,\"mobileAppPosition\",null,\"tabIndex\",null,\"container\",\"\",\"createdAt\",\"~m1662967609500\",\"updatedAt\",\"~m1662977816251\",\"folder\",\"apiUser\",\"screen\",null]]],\"list\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"list\",\"^4\",\"datasource\",\"^5\",\"NoSqlQuery\",\"^6\",null,\"^7\",\"a968b944-81f3-45be-9acc-e92dd5356d4a\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"method\",\"find\",\"lastReceivedFromResourceAt\",null,\"aggregation\",\"\",\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",true,\"showFailureToaster\",true,\"query\",\"{{searchQuery.value}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"update\",\"\",\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"useRawCollectionName\",false,\"data\",null,\"operations\",\"\",\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"projection\",\"{passwordHash: 0 }\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"sortBy\",\"\",\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[]],\"insert\",\"\",\"queryTimeout\",\"10000\",\"field\",\"\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"database\",\"\",\"changesetIsObject\",false,\"limit\",\"\",\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"options\",\"\",\"collection\",\"users\",\"skip\",\"\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653827447242\",\"^B\",\"~m1653842042013\",\"^C\",\"dbUser\",\"^D\",null]]],\"searchQuery\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"searchQuery\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"funcBody\",\"\\nconst escapeRegExp = (string) => {\\n return string.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g, '\\\\\\\\$&'); // $& means the whole matched string\\n}\\n\\nconst regexQuery = {\\n $regex: `.*${escapeRegExp({{search_bar.value}})}.*`,\\n $options:'i'\\n}\\n\\nreturn {\\n $or: [\\n { _id: regexQuery },\\n { firstName: regexQuery },\\n { lastName: regexQuery },\\n { email: regexQuery },\\n ],\\n}\\n\\n\",\"value\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653831767909\",\"^B\",\"~m1653841921344\",\"^C\",\"dbUser\",\"^D\",null]]],\"update\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"update\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"[{\\\"key\\\":\\\"firstName\\\",\\\"value\\\":\\\"{{firstNameInput.value}}\\\"},{\\\"key\\\":\\\"lastName\\\",\\\"value\\\":\\\"{{lastNameInput.value}}\\\"},{\\\"key\\\":\\\"email\\\",\\\"value\\\":\\\"{{emailInput.value}}\\\"}]\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/users/{{usersTable.selectedRow.data._id}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"userUpdateModal.close();\\nlist.trigger();\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"PUT\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653836736485\",\"^B\",\"~m1654498069760\",\"^C\",\"apiUser\",\"^D\",null]]],\"remove\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"remove\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"admin/users/{{usersTable.selectedRow.data._id}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"DELETE\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653840309251\",\"^B\",\"~m1654498060079\",\"^C\",\"apiUser\",\"^D\",null]]],\"verifyEmail\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"verifyEmail\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"2cb1c339-d07c-4308-92cd-78eeac59972e\",\"^8\",null,\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"account/verify-email/?token={{usersTable.selectedRow.data.signupToken}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"failure\",\"type\",\"script\",\"method\",\"run\",\"pluginId\",\"\",\"targetId\",null,\"params\",[\"^3\",[\"src\",\"list.trigger()\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"GET\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653838337897\",\"^B\",\"~m1653842008069\",\"^C\",\"apiUser\",\"^D\",null]]],\"welcomeText\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"welcomeText\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"bottom\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### Users Management\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",null,\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"header\",\"row\",0,\"col\",0,\"height\",0.6,\"width\",4,\"tabNum\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653827091457\",\"^B\",\"~m1653834455274\",\"^C\",\"\",\"^D\",null]]],\"usersTable\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"usersTable\",\"^4\",\"widget\",\"^5\",\"TableWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"showCustomButton\",true,\"sortMappedValue\",[\"^3\",[]],\"_filteredSortedRenderedDataWithTypes\",null,\"heightType\",\"fixed\",\"normalizedData\",null,\"rowHeight\",\"standard\",\"saveChangesDisabled\",\"\",\"columnTypeProperties\",[\"^3\",[]],\"columnWidths\",[\"^:\",[]],\"showSummaryFooter\",false,\"disableRowSelectInteraction\",false,\"columnWidthsMobile\",[\"^:\",[]],\"hasNextAfterCursor\",\"\",\"columnTypeSpecificExtras\",[\"^3\",[]],\"onRowAdded\",\"\",\"columnHeaderNames\",[\"^3\",[]],\"alwaysShowPaginator\",false,\"columnColors\",[\"^3\",[\"lastName\",\"\",\"signupToken\",\"\",\"createdOn\",\"\",\"passwordHash\",\"\",\"deletedOn\",\"\",\"lastRequest\",\"\",\"isEmailVerified\",\"\",\"_id\",\"\",\"updatedOn\",\"\",\"firstName\",\"\",\"email\",\"\"]],\"columnFrozenAlignments\",[\"^3\",[]],\"allowMultiRowSelect\",false,\"columnFormats\",[\"^3\",[]],\"columnRestrictedEditing\",[\"^3\",[]],\"showFilterButton\",true,\"_columnVisibility\",[\"^3\",[\"lastRequest\",true,\"signupToken\",false]],\"_columnSummaryTypes\",[\"^3\",[]],\"_columnsWithLegacyBackgroundColor\",[\"~#iOS\",[]],\"showAddRowButton\",false,\"_unfilteredSelectedIndex\",null,\"nextBeforeCursor\",\"\",\"columnVisibility\",[\"^3\",[]],\"selectedPageIndex\",\"0\",\"applyDynamicSettingsToColumnOrder\",true,\"rowColor\",[],\"actionButtonColumnName\",\"Actions\",\"resetAfterSave\",true,\"filterStackType\",\"and\",\"downloadRawData\",false,\"showFetchingIndicator\",true,\"serverPaginated\",false,\"data\",\"{{list.data}}\",\"displayedData\",null,\"actionButtons\",[\"^:\",[]],\"actionButtonSelectsRow\",true,\"selectRowByDefault\",true,\"defaultSortByColumn\",\"\",\"paginationOffset\",0,\"columnAlignment\",[\"^3\",[]],\"columnSummaries\",[\"^ \"],\"showBoxShadow\",true,\"sortedDesc\",false,\"customButtonName\",\"\",\"columnMappersRenderAsHTML\",[\"^3\",[]],\"showRefreshButton\",true,\"pageSize\",\"20\",\"useDynamicColumnSettings\",false,\"actionButtonPosition\",\"left\",\"dynamicRowHeights\",false,\"bulkUpdateAction\",\"\",\"afterCursor\",\"\",\"onCustomButtonPressQueryName\",\"\",\"changeSet\",[\"^ \"],\"sortedColumn\",\"\",\"_columnSummaryValues\",[\"^3\",[]],\"checkboxRowSelect\",true,\"_compatibilityMode\",false,\"showColumnBorders\",false,\"clearSelectionLabel\",\"Clear selection\",\"_renderedDataWithTypes\",null,\"columnAllowOverflow\",[\"^3\",[]],\"beforeCursor\",\"\",\"serverPaginationType\",\"limitOffsetBased\",\"onRowSelect\",\"\",\"showDownloadButton\",true,\"selectedIndex\",null,\"defaultSortDescending\",false,\"_sortedDisplayedDataIndices\",null,\"dynamicColumnSettings\",null,\"totalRowCount\",\"\",\"recordUpdates\",[],\"newRow\",null,\"emptyMessage\",\"No rows found\",\"columnEditable\",[\"^3\",[\"_id\",false,\"firstName\",false,\"lastRequest\",false,\"updatedOn\",false]],\"_viewerColumnSummaryTypes\",[\"^ \"],\"filters\",[],\"displayedDataIndices\",null,\"disableSorting\",[\"^3\",[]],\"columnMappers\",[\"^3\",[]],\"showClearSelection\",false,\"doubleClickToEdit\",true,\"overflowType\",\"pagination\",\"_reverseSortedDisplayedDataIndices\",null,\"showTableBorder\",true,\"selectedCell\",[\"^ \",\"index\",null,\"data\",null,\"columnName\",null],\"columns\",[\"^:\",[]],\"defaultSelectedRow\",\"first\",\"freezeActionButtonColumns\",false,\"sort\",null,\"_columns\",[\"^:\",[]],\"sortByRawValue\",[\"^3\",[]],\"calculatedColumns\",[\"^:\",[]],\"selectedRow\",[\"^ \",\"^K\",null,\"^L\",null],\"showPaginationOnTop\",false,\"_reverseDisplayedDataIndices\",null,\"nextAfterCursor\",\"\",\"useCompactMode\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",2.3999999999999995,\"col\",0,\"^G\",10.399999999999999,\"^H\",8,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653828995649\",\"^B\",\"~m1654498035519\",\"^C\",\"\",\"^D\",null]]],\"search_bar\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"search_bar\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Search by name, email or _id\",\"label\",\"\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",0.7999999999999999,\"col\",0,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653830224018\",\"^B\",\"~m1653830294498\",\"^C\",\"\",\"^D\",null]]],\"container1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"container1\",\"^4\",\"widget\",\"^5\",\"ContainerWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"_disabledByIndex\",[\"^:\",[\"\"]],\"heightType\",\"auto\",\"currentViewKey\",null,\"iconByIndex\",[],\"clickable\",false,\"_iconByIndex\",[\"^:\",[\"\"]],\"hidden\",\"\",\"showHeader\",true,\"hoistFetching\",true,\"views\",[],\"showInEditor\",false,\"tooltipText\",\"\",\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^:\",[\"\"]],\"currentViewIndex\",null,\"_hasMigratedNestedItems\",true,\"transition\",\"none\",\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^:\",[\"\"]],\"tooltipByIndex\",[],\"showFooter\",false,\"_viewKeys\",[\"^:\",[\"View 1\"]],\"events\",[\"^3\",[]],\"_ids\",[\"^:\",[\"6af98\"]],\"viewKeys\",[],\"iconPositionByIndex\",[],\"_iconPositionByIndex\",[\"^:\",[\"\"]],\"loading\",false,\"overflowType\",\"scroll\",\"disabled\",false,\"_labels\",[\"^:\",[\"\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"showBody\",true,\"labels\",[]]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",2.400000000000001,\"col\",8,\"^G\",10.4,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745012\",\"^B\",\"~m1653833745012\",\"^C\",\"\",\"^D\",null]]],\"containerTitle1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"containerTitle1\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### {{usersTable.selectedRow.data.firstName}} {{usersTable.selectedRow.data.lastName}} \",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"header\",\"^F\",\"\",\"row\",0.20000000000000012,\"col\",0,\"^G\",0.8,\"^H\",8,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745583\",\"^B\",\"~m1653838314223\",\"^C\",\"\",\"^D\",null]]],\"keyValue1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"keyValue1\",\"^4\",\"widget\",\"^5\",\"KeyValueMapWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"rowHeaderNames\",[\"^3\",[]],\"rowFormats\",[\"^3\",[]],\"valueTitle\",\"Value\",\"data\",\"{{usersTable.selectedRow.data}}\",\"prevRowMappers\",[\"^3\",[]],\"rowMappersRenderAsHTML\",[\"^3\",[]],\"rowVisibility\",[\"^3\",[\"a\",true,\"lastName\",true,\"signupToken\",true,\"b\",true,\"c\",true,\"createdOn\",true,\"deletedOn\",true,\"lastRequest\",true,\"isEmailVerified\",true,\"_id\",true,\"updatedOn\",true,\"firstName\",true,\"email\",true]],\"prevRowFormats\",[\"^3\",[]],\"rowMappers\",[\"^3\",[]],\"rows\",[\"^:\",[\"a\",\"b\",\"c\",\"_id\",\"firstName\",\"lastName\",\"email\",\"isEmailVerified\",\"createdOn\",\"updatedOn\",\"lastRequest\",\"signupToken\",\"deletedOn\"]],\"keyTitle\",\"Key\"]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"body\",\"^F\",\"6af98\",\"row\",0,\"col\",0,\"^G\",8.4,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833745869\",\"^B\",\"~m1654498035544\",\"^C\",\"\",\"^D\",null]]],\"userActions\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"userActions\",\"^4\",\"widget\",\"^5\",\"DropdownButtonWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"_disabledByIndex\",[\"^:\",[\"\",\"{{usersTable.selectedRow.data.isEmailVerified}}\",\"\",false]],\"horizontalAlign\",\"stretch\",\"_values\",[\"^:\",[\"\",\"\",\"\",\"Action 4\"]],\"iconByIndex\",[],\"iconPosition\",\"left\",\"clickable\",false,\"_iconByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"hidden\",false,\"data\",[],\"text\",\"Menu\",\"_fallbackTextByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"showInEditor\",false,\"tooltipText\",\"\",\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^:\",[\"\",\"\",\"\",false]],\"_captionByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"styleVariant\",\"outline\",\"_hasMigratedNestedItems\",true,\"captionByIndex\",[],\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"_colorByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"tooltipByIndex\",[],\"icon\",\"\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"shadowLogin\",\"targetId\",\"1faf9\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"remove\",\"targetId\",\"d452d\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"verifyEmail\",\"targetId\",\"ccb37\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"widget\",\"method\",\"open\",\"pluginId\",\"userUpdateModal\",\"targetId\",\"59b6e\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"_ids\",[\"^:\",[\"59b6e\",\"ccb37\",\"d452d\",\"1faf9\"]],\"overlayMaxHeight\",375,\"disabled\",false,\"_labels\",[\"^:\",[\"Update\",\"Verify Email\",\"Remove\",\"Shadow login\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"_imageByIndex\",[\"^:\",[\"\",\"\",\"\",\"\"]],\"labels\",[]]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^E\",\"header\",\"^F\",\"\",\"row\",0.2,\"col\",9,\"^G\",0.8,\"^H\",3,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653833840942\",\"^B\",\"~m1662970504941\",\"^C\",\"\",\"^D\",null]]],\"userUpdateModal\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"userUpdateModal\",\"^4\",\"widget\",\"^5\",\"ModalWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"opened\",false,\"modalOverflowType\",\"scroll\",\"hidden\",\"true\",\"onModalClose\",\"\",\"modalHeightType\",\"fixed\",\"tooltipText\",\"\",\"modalHeight\",\"\",\"onModalOpen\",\"\",\"modalWidth\",\"\",\"closeOnOutsideClick\",true,\"loading\",\"\",\"disabled\",\"\",\"buttonText\",\"Open Modal\"]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^E\",\"body\",\"^F\",\"\",\"row\",13.399999999999999,\"col\",8,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834649657\",\"^B\",\"~m1653834683865\",\"^C\",\"\",\"^D\",null]]],\"updateUserForm\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"updateUserForm\",\"^4\",\"widget\",\"^5\",\"FormWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"disableSubmit\",false,\"heightType\",\"auto\",\"resetAfterSubmit\",true,\"submitting\",false,\"hidden\",false,\"data\",[\"^ \"],\"showHeader\",true,\"hoistFetching\",true,\"initialData\",null,\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"border\",\"\"]],\"invalid\",false,\"showFooter\",true,\"events\",[\"^:\",[[\"^3\",[\"event\",\"submit\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"update\",\"targetId\",null,\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"overflowType\",\"scroll\",\"disabled\",false,\"requireValidation\",true,\"maintainSpaceWhenHidden\",false,\"showBody\",true]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"userUpdateModal\",\"^E\",\"body\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",0.2,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834910846\",\"^B\",\"~m1653841961694\",\"^C\",\"\",\"^D\",null]]],\"formTitle1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"formTitle1\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"#### Update {{ usersTable.selectedRow.data.firstName}} {{usersTable.selectedRow.data.lastName}}\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"header\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",0.6,\"^H\",12,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834911218\",\"^B\",\"~m1653838314310\",\"^C\",\"\",\"^D\",null]]],\"formButton1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"formButton1\",\"^4\",\"widget\",\"^5\",\"ButtonWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"horizontalAlign\",\"stretch\",\"clickable\",false,\"iconAfter\",\"\",\"submitTargetId\",\"updateUserForm\",\"hidden\",false,\"text\",\"Submit\",\"showInEditor\",false,\"tooltipText\",\"\",\"submit\",true,\"iconBefore\",\"\",\"events\",[\"^3\",[]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"footer\",\"^F\",\"\",\"row\",0.40000000000000036,\"col\",8,\"^G\",1,\"^H\",4,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653834911337\",\"^B\",\"~m1653835572234\",\"^C\",\"\",\"^D\",null]]],\"firstNameInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"firstNameInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{usersTable.selectedRow.data.firstName}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Enter value\",\"label\",\"First Name\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",0,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653835222740\",\"^B\",\"~m1653838314340\",\"^C\",\"\",\"^D\",null]]],\"lastNameInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"lastNameInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\" {{usersTable.selectedRow.data.lastName}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Enter value\",\"label\",\"Last Name\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",1,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653835244433\",\"^B\",\"~m1653838314371\",\"^C\",\"\",\"^D\",null]]],\"emailInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"emailInput\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",false,\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"\",\"patternType\",\"email\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{usersTable.selectedRow.data.email}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"you@example.com\",\"label\",\"Email\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"bold/mail-send-envelope\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"updateUserForm\",\"^E\",\"body\",\"^F\",\"\",\"row\",2,\"col\",0,\"^G\",1,\"^H\",10,\"^I\",0]]],\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1653836295252\",\"^B\",\"~m1653838314403\",\"^C\",\"\",\"^D\",null]]],\"$main\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$main\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"type\",\"main\",\"sticky\",false]],\"^;\",[\"^3\",[]],\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1662967417421\",\"^B\",\"~m1662967417421\",\"^C\",\"\",\"^D\",null]]],\"$header\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$header\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"type\",\"header\",\"sticky\",true]],\"^;\",[\"^3\",[]],\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",null,\"^@\",\"\",\"^A\",\"~m1662967417421\",\"^B\",\"~m1662967417421\",\"^C\",\"\",\"^D\",null]]]]],\"^A\",null,\"version\",\"2.99.1\",\"appThemeId\",null,\"preloadedAppJavaScript\",null,\"preloadedAppJSLinks\",[],\"testEntities\",[],\"tests\",[],\"appStyles\",\"\",\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^:\",[]],\"pageLoadValueOverrides\",[\"^:\",[]],\"customDocumentTitle\",\"\",\"customDocumentTitleEnabled\",false,\"customShortcuts\",[],\"isGlobalWidget\",false,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"folders\",[\"^:\",[\"dbUser\",\"apiUser\"]],\"queryStatusVisibility\",true,\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null,\"instrumentationEnabled\",false,\"experimentalPerfFeatures\",[\"^ \",\"batchCommitModelEnabled\",false,\"skipDepCycleCheckingEnabled\",false,\"serverDepGraphEnabled\",false,\"useRuntimeV2\",false],\"experimentalDataTabEnabled\",false]]]" + }, + "changesRecord": [ + { + "type": "DATASOURCE_TYPE_CHANGE", + "payload": { + "newType": "RESTQuery", + "pluginId": "shadowLogin", + "resourceName": "bb8ab17c-b112-4444-b054-3067ca2b62dc" + } + }, + { + "type": "WIDGET_TEMPLATE_UPDATE", + "payload": { + "plugin": { + "id": "shadowLogin", + "type": "datasource", + "style": null, + "folder": "apiUser", + "screen": null, + "subtype": "RESTQuery", + "tabIndex": null, + "template": { + "body": "", + "data": null, + "type": "GET", + "error": null, + "query": "", + "events": [], + "cookies": "", + "headers": "", + "rawData": null, + "bodyType": "json", + "metadata": null, + "changeset": "", + "timestamp": 0, + "isFetching": false, + "isImported": false, + "cacheKeyTtl": "", + "transformer": "// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data", + "queryTimeout": "10000", + "allowedGroups": [], + "enableCaching": false, + "privateParams": [], + "queryDisabled": "", + "watchedParams": [], + "successMessage": "", + "changesetObject": "", + "paginationLimit": "", + "errorTransformer": "// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error", + "queryRefreshTime": "", + "runWhenPageLoads": false, + "changesetIsObject": false, + "enableTransformer": false, + "paginationEnabled": false, + "queryThrottleTime": "750", + "queryTriggerDelay": "0", + "showFailureToaster": true, + "showSuccessToaster": true, + "importedQueryInputs": {}, + "paginationDataField": "", + "playgroundQueryUuid": "", + "requireConfirmation": false, + "runWhenModelUpdates": true, + "notificationDuration": "", + "queryDisabledMessage": "", + "resourceNameOverride": "", + "importedQueryDefaults": {}, + "playgroundQuerySaveId": "latest", + "runWhenPageLoadsDelay": "", + "enableErrorTransformer": false, + "queryFailureConditions": "", + "paginationPaginationField": "", + "updateSetValueDynamically": false, + "showLatestVersionUpdatedWarning": false, + "showUpdateSetValueDynamicallyToggle": true + }, + "container": "", + "createdAt": "2022-09-12T07:26:49.500Z", + "namespace": null, + "position2": null, + "updatedAt": "2022-09-12T10:14:09.072Z", + "resourceName": "bb8ab17c-b112-4444-b054-3067ca2b62dc", + "mobilePosition2": null, + "mobileAppPosition": null, + "resourceDisplayName": null + }, + "update": { + "body": "[{\"key\":\"id\",\"value\":\"\\\"6315f482ec588c3bcb5b07a8\\\"\"}]", + "data": null, + "type": "POST", + "error": null, + "query": "admin/account/shadow-login", + "events": [], + "cookies": "", + "headers": "", + "rawData": null, + "bodyType": "json", + "metadata": null, + "changeset": "", + "timestamp": 0, + "isFetching": false, + "isImported": false, + "cacheKeyTtl": "", + "transformer": "// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data", + "queryTimeout": "10000", + "allowedGroups": [], + "enableCaching": false, + "privateParams": [], + "queryDisabled": "", + "watchedParams": [], + "successMessage": "", + "changesetObject": "", + "paginationLimit": "", + "errorTransformer": "// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error", + "queryRefreshTime": "", + "runWhenPageLoads": false, + "changesetIsObject": false, + "enableTransformer": false, + "paginationEnabled": false, + "playgroundQueryId": null, + "queryThrottleTime": "750", + "queryTriggerDelay": "0", + "showFailureToaster": true, + "showSuccessToaster": true, + "confirmationMessage": null, + "importedQueryInputs": {}, + "paginationDataField": "", + "playgroundQueryUuid": "", + "requireConfirmation": false, + "runWhenModelUpdates": false, + "notificationDuration": "", + "queryDisabledMessage": "", + "resourceNameOverride": "", + "resourceTypeOverride": "", + "importedQueryDefaults": {}, + "playgroundQuerySaveId": "latest", + "runWhenPageLoadsDelay": "", + "enableErrorTransformer": false, + "queryFailureConditions": "", + "paginationPaginationField": "", + "updateSetValueDynamically": false, + "lastReceivedFromResourceAt": null, + "showLatestVersionUpdatedWarning": false, + "showUpdateSetValueDynamicallyToggle": true + }, + "widgetId": "shadowLogin", + "shouldRecalculateTemplate": true + }, + "isUserTriggered": true + } + ], + "gitSha": null, + "checksum": null, + "createdAt": "2022-09-12T10:16:56.671Z", + "updatedAt": "2022-09-12T10:16:56.671Z", + "pageId": 1427949, + "userId": 403318, + "branchId": null + }, + "modules": {} +} diff --git a/template/apps/api/src/db.ts b/template/apps/api/src/db.ts index 27f50afa..c309453c 100644 --- a/template/apps/api/src/db.ts +++ b/template/apps/api/src/db.ts @@ -1,4 +1,4 @@ -import { Database, Service, ServiceOptions, IDocument } from '@paralect/node-mongo'; +import { Database, IDocument, Service, ServiceOptions } from '@paralect/node-mongo'; import config from 'config'; diff --git a/template/apps/api/src/globals.d.ts b/template/apps/api/src/globals.d.ts index d277397b..6cff7661 100644 --- a/template/apps/api/src/globals.d.ts +++ b/template/apps/api/src/globals.d.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-var */ +/* eslint-disable no-var, vars-on-top */ import type { Logger } from 'winston'; declare global { diff --git a/template/apps/api/src/io-emitter.ts b/template/apps/api/src/io-emitter.ts index ad3e03f4..a1616b47 100644 --- a/template/apps/api/src/io-emitter.ts +++ b/template/apps/api/src/io-emitter.ts @@ -1,7 +1,6 @@ import { Emitter } from '@socket.io/redis-emitter'; - -import redisClient, { redisErrorHandler } from 'redis-client'; import logger from 'logger'; +import redisClient, { redisErrorHandler } from 'redis-client'; let emitter: Emitter | null = null; diff --git a/template/apps/api/src/koa-qs.d.ts b/template/apps/api/src/koa-qs.d.ts index 9da50d1c..a7c773b2 100644 --- a/template/apps/api/src/koa-qs.d.ts +++ b/template/apps/api/src/koa-qs.d.ts @@ -1,7 +1,7 @@ import AppKoa from 'types'; declare namespace koaQs { - type ParseMode = 'extended' | 'strict' | 'first'; + type ParseMode = 'extended' | 'strict' | 'first'; } declare function koaQs(app: AppKoa, mode?: koaQs.ParseMode): AppKoa; diff --git a/template/apps/api/src/middlewares/index.ts b/template/apps/api/src/middlewares/index.ts index b044bfa9..6c1fc5b9 100644 --- a/template/apps/api/src/middlewares/index.ts +++ b/template/apps/api/src/middlewares/index.ts @@ -1,7 +1,4 @@ -import validateMiddleware from './validate.middleware'; import rateLimitMiddleware from './rateLimit.middleware'; +import validateMiddleware from './validate.middleware'; -export { - validateMiddleware, - rateLimitMiddleware, -}; +export { rateLimitMiddleware, validateMiddleware }; diff --git a/template/apps/api/src/middlewares/validate.middleware.ts b/template/apps/api/src/middlewares/validate.middleware.ts index 32c491f0..77e624b3 100644 --- a/template/apps/api/src/middlewares/validate.middleware.ts +++ b/template/apps/api/src/middlewares/validate.middleware.ts @@ -1,4 +1,4 @@ -import { ZodSchema, ZodError, ZodIssue } from 'zod'; +import { ZodError, ZodIssue, ZodSchema } from 'zod'; import { AppKoaContext, Next, ValidationErrors } from 'types'; @@ -20,7 +20,7 @@ const formatError = (zodError: ZodError): ValidationErrors => { const validate = (schema: ZodSchema) => async (ctx: AppKoaContext, next: Next) => { const result = await schema.safeParseAsync({ - ...ctx.request.body as object, + ...(ctx.request.body as object), ...ctx.query, ...ctx.params, }); diff --git a/template/apps/api/src/migrator.ts b/template/apps/api/src/migrator.ts index baef3c6c..3ca40bd8 100644 --- a/template/apps/api/src/migrator.ts +++ b/template/apps/api/src/migrator.ts @@ -1,12 +1,13 @@ // allows to require modules relative to /src folder // for example: require('lib/mongo/idGenerator') // all options can be found here: https://gist.github.com/branneman/8048520 -import moduleAlias from 'module-alias'; -moduleAlias.addPath(__dirname); -moduleAlias(); // read aliases from package json +import moduleAlias from 'module-alias'; // read aliases from package json + +import migrator from 'migrator/index'; import 'dotenv/config'; -import migrator from 'migrator/index'; +moduleAlias.addPath(__dirname); +moduleAlias(); migrator.exec(); diff --git a/template/apps/api/src/migrator/index.ts b/template/apps/api/src/migrator/index.ts index 50897763..65bbceae 100644 --- a/template/apps/api/src/migrator/index.ts +++ b/template/apps/api/src/migrator/index.ts @@ -1,13 +1,12 @@ -import moment from 'moment'; -import 'moment-duration-format'; import { generateId } from '@paralect/node-mongo'; - import logger from 'logger'; -import db from 'db'; +import moment from 'moment'; -import { Migration } from './types'; import migrationLogService from './migration-log/migration-log.service'; import migrationVersionService from './migration-version/migration-version.service'; +import { Migration } from './types'; + +import 'moment-duration-format'; interface Duration extends moment.Duration { format: (template?: string, precision?: number, settings?: DurationSettings) => string; @@ -21,7 +20,8 @@ interface DurationSettings { } const run = async (migrations: Migration[], curVersion: number) => { - const newMigrations = migrations.filter((migration: Migration) => migration.version > curVersion) + const newMigrations = migrations + .filter((migration: Migration) => migration.version > curVersion) .sort((a: Migration, b: Migration) => a.version - b.version); if (!newMigrations.length) { @@ -35,7 +35,8 @@ const run = async (migrations: Migration[], curVersion: number) => { let lastMigrationVersion; try { - for (migration of newMigrations) { //eslint-disable-line + for (migration of newMigrations) { + //eslint-disable-line migrationLogId = generateId(); const startTime = new Date().getSeconds(); await migrationLogService.startMigrationLog(migrationLogId, startTime, migration.version); //eslint-disable-line @@ -48,8 +49,9 @@ const run = async (migrations: Migration[], curVersion: number) => { lastMigrationVersion = migration.version; await migrationVersionService.setNewMigrationVersion(migration.version); //eslint-disable-line const finishTime = new Date().getSeconds(); - const duration = (moment.duration(finishTime - startTime) as Duration) - .format('h [hrs], m [min], s [sec], S [ms]'); + const duration = (moment.duration(finishTime - startTime) as Duration).format( + 'h [hrs], m [min], s [sec], S [ms]', + ); await migrationLogService.finishMigrationLog(migrationLogId, finishTime, duration); //eslint-disable-line logger.info(`[Migrator] Database has been updated to the version #${migration.version}`); diff --git a/template/apps/api/src/migrator/migration-log/migration-log.service.ts b/template/apps/api/src/migrator/migration-log/migration-log.service.ts index dbb8940d..ed73da82 100644 --- a/template/apps/api/src/migrator/migration-log/migration-log.service.ts +++ b/template/apps/api/src/migrator/migration-log/migration-log.service.ts @@ -20,7 +20,8 @@ const startMigrationLog = (_id: string, startTime: number, migrationVersion: num _id, }, }, - {}, { upsert: true }, + {}, + { upsert: true }, ); const failMigrationLog = (_id: string, finishTime: number, err: Error) => diff --git a/template/apps/api/src/migrator/migration-version/migration-version.service.ts b/template/apps/api/src/migrator/migration-version/migration-version.service.ts index c96e593a..7eaedf53 100644 --- a/template/apps/api/src/migrator/migration-version/migration-version.service.ts +++ b/template/apps/api/src/migrator/migration-version/migration-version.service.ts @@ -1,12 +1,13 @@ -import db from 'db'; import fs from 'fs'; import path from 'path'; +import { Migration } from 'migrator/types'; + +import db from 'db'; + import schema from './migration-version.schema'; import { MigrationVersion } from './migration-version-types'; -import { Migration } from 'migrator/types'; - const service = db.createService('__migrationVersion', { schemaValidator: (obj) => schema.parseAsync(obj), }); @@ -14,12 +15,10 @@ const service = db.createService('__migrationVersion', { const migrationPaths = path.join(__dirname, '../migrations'); const id = 'migration_version'; -const getMigrationNames = (): string[] => { - return fs.readdirSync(migrationPaths).filter(file => !file.endsWith('.js.map')); -}; +const getMigrationNames = (): string[] => fs.readdirSync(migrationPaths).filter((file) => !file.endsWith('.js.map')); -const getCurrentMigrationVersion = () => service.findOne({ _id: id }) - .then((doc: MigrationVersion | null) => { +const getCurrentMigrationVersion = () => + service.findOne({ _id: id }).then((doc: MigrationVersion | null) => { if (!doc) { return 0; } @@ -49,7 +48,9 @@ const setNewMigrationVersion = (version: number) => $setOnInsert: { _id: id, }, - }, {}, { upsert: true }, + }, + {}, + { upsert: true }, ); export default Object.assign(service, { diff --git a/template/apps/api/src/migrator/migrations/1.ts b/template/apps/api/src/migrator/migrations/1.ts index bc305c82..98fb7eb0 100644 --- a/template/apps/api/src/migrator/migrations/1.ts +++ b/template/apps/api/src/migrator/migrations/1.ts @@ -11,10 +11,8 @@ migration.migrate = async () => { isEmailVerified: true, }); - const updateFn = (userId: string) => userService.atomic.updateOne( - { _id: userId }, - { $set: { isEmailVerified: false } }, - ); + const updateFn = (userId: string) => + userService.atomic.updateOne({ _id: userId }, { $set: { isEmailVerified: false } }); await promiseUtil.promiseLimit(userIds, 50, updateFn); }; diff --git a/template/apps/api/src/redis-client.ts b/template/apps/api/src/redis-client.ts index de4e765d..23c1d5db 100644 --- a/template/apps/api/src/redis-client.ts +++ b/template/apps/api/src/redis-client.ts @@ -1,12 +1,12 @@ import { Redis } from 'ioredis'; +import logger from 'logger'; import config from 'config'; -import logger from 'logger'; const client = new Redis(config.REDIS_URI as string, { lazyConnect: true, retryStrategy: (times) => { - if (times > 20) return; + if (times > 20) return null; return Math.max(Math.min(Math.exp(times), 15_000), 1_000); }, diff --git a/template/apps/api/src/resources/account/account.routes.ts b/template/apps/api/src/resources/account/account.routes.ts index f6d1cf75..39f2bb3d 100644 --- a/template/apps/api/src/resources/account/account.routes.ts +++ b/template/apps/api/src/resources/account/account.routes.ts @@ -1,19 +1,19 @@ import { routeUtil } from 'utils'; +import forgotPassword from './actions/forgot-password'; import get from './actions/get'; -import update from './actions/update'; -import uploadAvatar from './actions/upload-avatar'; +import google from './actions/google'; import removeAvatar from './actions/remove-avatar'; -import signUp from './actions/sign-up'; +import resendEmail from './actions/resend-email'; +import resetPassword from './actions/reset-password'; +import shadowLogin from './actions/shadow-login'; import signIn from './actions/sign-in'; import signOut from './actions/sign-out'; +import signUp from './actions/sign-up'; +import update from './actions/update'; +import uploadAvatar from './actions/upload-avatar'; import verifyEmail from './actions/verify-email'; -import forgotPassword from './actions/forgot-password'; -import resetPassword from './actions/reset-password'; import verifyResetToken from './actions/verify-reset-token'; -import resendEmail from './actions/resend-email'; -import shadowLogin from './actions/shadow-login'; -import google from './actions/google'; const publicRoutes = routeUtil.getRoutes([ signUp, @@ -27,16 +27,9 @@ const publicRoutes = routeUtil.getRoutes([ google, ]); -const privateRoutes = routeUtil.getRoutes([ - get, - update, - uploadAvatar, - removeAvatar, -]); +const privateRoutes = routeUtil.getRoutes([get, update, uploadAvatar, removeAvatar]); -const adminRoutes = routeUtil.getRoutes([ - shadowLogin, -]); +const adminRoutes = routeUtil.getRoutes([shadowLogin]); export default { publicRoutes, diff --git a/template/apps/api/src/resources/account/actions/forgot-password.ts b/template/apps/api/src/resources/account/actions/forgot-password.ts index 62e36243..f7b3c720 100644 --- a/template/apps/api/src/resources/account/actions/forgot-password.ts +++ b/template/apps/api/src/resources/account/actions/forgot-password.ts @@ -1,8 +1,5 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, Template, User } from 'types'; -import { EMAIL_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; @@ -11,6 +8,9 @@ import { securityUtil } from 'utils'; import config from 'config'; +import { EMAIL_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next, Template, User } from 'types'; + const schema = z.object({ email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), }); @@ -22,7 +22,10 @@ interface ValidatedData extends z.infer { async function validator(ctx: AppKoaContext, next: Next) { const user = await userService.findOne({ email: ctx.validatedData.email }); - if (!user) return ctx.status = 204; + if (!user) { + ctx.status = 204; + return; + } ctx.validatedData.user = user; await next(); diff --git a/template/apps/api/src/resources/account/actions/get.ts b/template/apps/api/src/resources/account/actions/get.ts index a87bfe35..938fe04e 100644 --- a/template/apps/api/src/resources/account/actions/get.ts +++ b/template/apps/api/src/resources/account/actions/get.ts @@ -1,7 +1,7 @@ -import { AppKoaContext, AppRouter } from 'types'; - import { userService } from 'resources/user'; +import { AppKoaContext, AppRouter } from 'types'; + async function handler(ctx: AppKoaContext) { ctx.body = { ...userService.getPublic(ctx.state.user), diff --git a/template/apps/api/src/resources/account/actions/google.ts b/template/apps/api/src/resources/account/actions/google.ts index 09e51ba8..f15ab71d 100644 --- a/template/apps/api/src/resources/account/actions/google.ts +++ b/template/apps/api/src/resources/account/actions/google.ts @@ -1,23 +1,16 @@ -import { AppRouter, AppKoaContext } from 'types'; - import { userService } from 'resources/user'; -import { googleService, authService } from 'services'; +import { authService, googleService } from 'services'; import config from 'config'; -type ValidatedData = { - given_name: string; - family_name: string; - email: string; - picture: string -}; +import { AppKoaContext, AppRouter } from 'types'; const getOAuthUrl = async (ctx: AppKoaContext) => { - const isValidCredentials = config.GOOGLE_CLIENT_ID && config.GOOGLE_CLIENT_SECRET; + const areCredentialsExist = config.GOOGLE_CLIENT_ID && config.GOOGLE_CLIENT_SECRET; - ctx.assertClientError(isValidCredentials, { - global: 'Setup Google Oauth credentials on API', + ctx.assertClientError(areCredentialsExist, { + global: 'Setup Google OAuth credentials on API', }); ctx.redirect(googleService.oAuthURL); @@ -26,55 +19,51 @@ const getOAuthUrl = async (ctx: AppKoaContext) => { const signInGoogleWithCode = async (ctx: AppKoaContext) => { const { code } = ctx.request.query; - const { isValid, payload } = await googleService. - exchangeCodeForToken(code as string) as { isValid: boolean, payload: ValidatedData }; + const { isValid, payload } = await googleService.exchangeCodeForToken(code); - ctx.assertError(isValid, `Exchange code for token error: ${payload}`); + ctx.assertError(isValid && payload && !(payload instanceof Error), `Exchange code for token error: ${payload}`); const user = await userService.findOne({ email: payload.email }); let userChanged; if (user) { if (!user.oauth?.google) { - userChanged = await userService.updateOne( - { _id: user._id }, - (old) => ({ ...old, oauth: { google: true } }), - ); + userChanged = await userService.updateOne({ _id: user._id }, (old) => ({ + ...old, + oauth: { google: true }, + })); } + const userUpdated = userChanged || user; - await Promise.all([ - userService.updateLastRequest(userUpdated._id), - authService.setTokens(ctx, userUpdated._id), - ]); - - } else { - const lastName = payload.family_name || ''; - const fullName = lastName ? `${payload.given_name} ${lastName}` : payload.given_name; - - const newUser = await userService.insertOne({ - firstName: payload.given_name, - lastName, - fullName, - email: payload.email, - isEmailVerified: true, - avatarUrl: payload.picture, - oauth: { - google: true, - }, - }); - - if (newUser) { - await Promise.all([ - userService.updateLastRequest(newUser._id), - authService.setTokens(ctx, newUser._id), - ]); - } + + await Promise.all([userService.updateLastRequest(userUpdated._id), authService.setTokens(ctx, userUpdated._id)]); + + return; } - ctx.redirect(config.WEB_URL); -}; + const { givenName: firstName, familyName, email, picture: avatarUrl } = payload; + + const lastName = familyName || ''; + const fullName = lastName ? `${firstName} ${lastName}` : firstName; + + const newUser = await userService.insertOne({ + firstName, + lastName, + fullName, + email, + isEmailVerified: true, + avatarUrl, + oauth: { + google: true, + }, + }); + if (newUser) { + await Promise.all([userService.updateLastRequest(newUser._id), authService.setTokens(ctx, newUser._id)]); + } + ctx.redirect(config.WEB_URL); +}; export default (router: AppRouter) => { router.get('/sign-in/google/auth', getOAuthUrl); diff --git a/template/apps/api/src/resources/account/actions/remove-avatar.ts b/template/apps/api/src/resources/account/actions/remove-avatar.ts index 2c3353e1..189d649b 100644 --- a/template/apps/api/src/resources/account/actions/remove-avatar.ts +++ b/template/apps/api/src/resources/account/actions/remove-avatar.ts @@ -1,13 +1,13 @@ -import { AppKoaContext, Next, AppRouter } from 'types'; - import { userService } from 'resources/user'; import { cloudStorageService } from 'services'; +import { AppKoaContext, AppRouter, Next } from 'types'; + async function validator(ctx: AppKoaContext, next: Next) { const { user } = ctx.state; - ctx.assertClientError(user.avatarUrl, { global: 'You don\'t have avatar' }); + ctx.assertClientError(user.avatarUrl, { global: "You don't have an avatar" }); await next(); } @@ -15,7 +15,7 @@ async function validator(ctx: AppKoaContext, next: Next) { async function handler(ctx: AppKoaContext) { const { user } = ctx.state; - const fileKey = cloudStorageService.helpers.getFileKey(user.avatarUrl || ''); + const fileKey = cloudStorageService.getFileKey(user.avatarUrl); const [updatedUser] = await Promise.all([ userService.updateOne({ _id: user._id }, () => ({ avatarUrl: null })), diff --git a/template/apps/api/src/resources/account/actions/resend-email.ts b/template/apps/api/src/resources/account/actions/resend-email.ts index 49b4ac1b..cd7b16bd 100644 --- a/template/apps/api/src/resources/account/actions/resend-email.ts +++ b/template/apps/api/src/resources/account/actions/resend-email.ts @@ -1,8 +1,5 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, Template, User } from 'types'; -import { EMAIL_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; @@ -11,6 +8,9 @@ import { securityUtil } from 'utils'; import config from 'config'; +import { EMAIL_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next, Template, User } from 'types'; + const schema = z.object({ email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), }); @@ -24,7 +24,10 @@ async function validator(ctx: AppKoaContext, next: Next) { const user = await userService.findOne({ email }); - if (!user) return ctx.status = 204; + if (!user) { + ctx.status = 204; + return; + } ctx.validatedData.user = user; await next(); diff --git a/template/apps/api/src/resources/account/actions/reset-password.ts b/template/apps/api/src/resources/account/actions/reset-password.ts index 02892151..6af145f3 100644 --- a/template/apps/api/src/resources/account/actions/reset-password.ts +++ b/template/apps/api/src/resources/account/actions/reset-password.ts @@ -1,19 +1,21 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, User } from 'types'; -import { PASSWORD_REGEX } from 'app-constants'; - -import { userService } from 'resources/user'; +import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; import { securityUtil } from 'utils'; +import { PASSWORD_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next, User } from 'types'; + const schema = z.object({ token: z.string().min(1, 'Token is required'), - password: z.string().regex( - PASSWORD_REGEX, - 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', - ), + password: z + .string() + .regex( + PASSWORD_REGEX, + 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', + ), }); interface ValidatedData extends z.infer { @@ -25,7 +27,10 @@ async function validator(ctx: AppKoaContext, next: Next) { const user = await userService.findOne({ resetPasswordToken: token }); - if (!user) return ctx.status = 204; + if (!user) { + ctx.status = 204; + return; + } ctx.validatedData.user = user; await next(); diff --git a/template/apps/api/src/resources/account/actions/shadow-login.ts b/template/apps/api/src/resources/account/actions/shadow-login.ts index a89c046d..d5f961cb 100644 --- a/template/apps/api/src/resources/account/actions/shadow-login.ts +++ b/template/apps/api/src/resources/account/actions/shadow-login.ts @@ -1,7 +1,5 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, User } from 'types'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; @@ -9,6 +7,8 @@ import { authService } from 'services'; import config from 'config'; +import { AppKoaContext, AppRouter, Next, User } from 'types'; + const schema = z.object({ id: z.string().min(1, 'User ID is required'), }); diff --git a/template/apps/api/src/resources/account/actions/sign-in.ts b/template/apps/api/src/resources/account/actions/sign-in.ts index 482c8deb..ff7ef2b3 100644 --- a/template/apps/api/src/resources/account/actions/sign-in.ts +++ b/template/apps/api/src/resources/account/actions/sign-in.ts @@ -1,20 +1,22 @@ import { z } from 'zod'; -import { AppKoaContext, AppRouter, Next, User } from 'types'; -import { EMAIL_REGEX, PASSWORD_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { rateLimitMiddleware, validateMiddleware } from 'middlewares'; import { authService } from 'services'; import { securityUtil } from 'utils'; +import { EMAIL_REGEX, PASSWORD_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next, User } from 'types'; + const schema = z.object({ email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), - password: z.string().regex( - PASSWORD_REGEX, - 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', - ), + password: z + .string() + .regex( + PASSWORD_REGEX, + 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', + ), }); interface ValidatedData extends z.infer { @@ -47,10 +49,7 @@ async function validator(ctx: AppKoaContext, next: Next) { async function handler(ctx: AppKoaContext) { const { user } = ctx.validatedData; - await Promise.all([ - userService.updateLastRequest(user._id), - authService.setTokens(ctx, user._id), - ]); + await Promise.all([userService.updateLastRequest(user._id), authService.setTokens(ctx, user._id)]); ctx.body = userService.getPublic(user); } diff --git a/template/apps/api/src/resources/account/actions/sign-out.ts b/template/apps/api/src/resources/account/actions/sign-out.ts index 4e050591..217babea 100644 --- a/template/apps/api/src/resources/account/actions/sign-out.ts +++ b/template/apps/api/src/resources/account/actions/sign-out.ts @@ -1,7 +1,7 @@ -import { AppKoaContext, AppRouter } from 'types'; - import { authService } from 'services'; +import { AppKoaContext, AppRouter } from 'types'; + const handler = async (ctx: AppKoaContext) => { await authService.unsetTokens(ctx); diff --git a/template/apps/api/src/resources/account/actions/sign-up.ts b/template/apps/api/src/resources/account/actions/sign-up.ts index 109a577c..854fb602 100644 --- a/template/apps/api/src/resources/account/actions/sign-up.ts +++ b/template/apps/api/src/resources/account/actions/sign-up.ts @@ -1,8 +1,5 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, Template } from 'types'; -import { EMAIL_REGEX, PASSWORD_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; @@ -11,14 +8,19 @@ import { securityUtil } from 'utils'; import config from 'config'; +import { EMAIL_REGEX, PASSWORD_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next, Template } from 'types'; + const schema = z.object({ firstName: z.string().min(1, 'Please enter First name').max(100), lastName: z.string().min(1, 'Please enter Last name').max(100), email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), - password: z.string().regex( - PASSWORD_REGEX, - 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', - ), + password: z + .string() + .regex( + PASSWORD_REGEX, + 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', + ), }); type ValidatedData = z.infer; @@ -36,17 +38,9 @@ async function validator(ctx: AppKoaContext, next: Next) { } async function handler(ctx: AppKoaContext) { - const { - firstName, - lastName, - email, - password, - } = ctx.validatedData; + const { firstName, lastName, email, password } = ctx.validatedData; - const [hash, signupToken] = await Promise.all([ - securityUtil.getHash(password), - securityUtil.generateSecureToken(), - ]); + const [hash, signupToken] = await Promise.all([securityUtil.getHash(password), securityUtil.generateSecureToken()]); const user = await userService.insertOne({ email, diff --git a/template/apps/api/src/resources/account/actions/update.ts b/template/apps/api/src/resources/account/actions/update.ts index 7dbad5dc..73f97b85 100644 --- a/template/apps/api/src/resources/account/actions/update.ts +++ b/template/apps/api/src/resources/account/actions/update.ts @@ -1,22 +1,27 @@ import _ from 'lodash'; import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter } from 'types'; -import { PASSWORD_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; import { securityUtil } from 'utils'; -const schema = z.object({ - firstName: z.string().min(1, 'Please enter First name').max(100).optional(), - lastName: z.string().min(1, 'Please enter Last name').max(100).optional(), - password: z.string().regex( - PASSWORD_REGEX, - 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', - ).optional(), -}).strict(); +import { PASSWORD_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next } from 'types'; + +const schema = z + .object({ + firstName: z.string().min(1, 'Please enter First name').max(100).optional(), + lastName: z.string().min(1, 'Please enter Last name').max(100).optional(), + password: z + .string() + .regex( + PASSWORD_REGEX, + 'The password must contain 6 or more characters with at least one letter (a-z) and one number (0-9).', + ) + .optional(), + }) + .strict(); interface ValidatedData extends z.infer { passwordHash?: string | null; @@ -44,10 +49,7 @@ async function validator(ctx: AppKoaContext, next: Next) { async function handler(ctx: AppKoaContext) { const { user } = ctx.state; - const updatedUser = await userService.updateOne( - { _id: user._id }, - () => _.pickBy(ctx.validatedData), - ); + const updatedUser = await userService.updateOne({ _id: user._id }, () => _.pickBy(ctx.validatedData)); ctx.body = userService.getPublic(updatedUser); } diff --git a/template/apps/api/src/resources/account/actions/upload-avatar.ts b/template/apps/api/src/resources/account/actions/upload-avatar.ts index 170234ac..35db0cfa 100644 --- a/template/apps/api/src/resources/account/actions/upload-avatar.ts +++ b/template/apps/api/src/resources/account/actions/upload-avatar.ts @@ -1,11 +1,11 @@ import multer from '@koa/multer'; -import { Next, AppKoaContext, AppRouter } from 'types'; - import { userService } from 'resources/user'; import { cloudStorageService } from 'services'; +import { AppKoaContext, AppRouter, Next } from 'types'; + const upload = multer(); async function validator(ctx: AppKoaContext, next: Next) { @@ -21,20 +21,15 @@ async function handler(ctx: AppKoaContext) { const { file } = ctx.request; if (user.avatarUrl) { - const fileKey = cloudStorageService.helpers.getFileKey(user.avatarUrl); + const fileKey = cloudStorageService.getFileKey(user.avatarUrl); await cloudStorageService.deleteObject(fileKey); } const fileName = `${user._id}-${Date.now()}-${file.originalname}`; - const { Location } = await cloudStorageService.uploadPublic(`avatars/${fileName}`, file); - - const updatedUser = await userService.updateOne( - { _id: user._id }, - () => ({ avatarUrl: Location }), - ); + const { location: avatarUrl } = await cloudStorageService.uploadPublic(`avatars/${fileName}`, file); - ctx.body = userService.getPublic(updatedUser); + ctx.body = await userService.updateOne({ _id: user._id }, () => ({ avatarUrl })).then(userService.getPublic); } export default (router: AppRouter) => { diff --git a/template/apps/api/src/resources/account/actions/verify-email.ts b/template/apps/api/src/resources/account/actions/verify-email.ts index d6fcf604..408df22e 100644 --- a/template/apps/api/src/resources/account/actions/verify-email.ts +++ b/template/apps/api/src/resources/account/actions/verify-email.ts @@ -1,7 +1,5 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter, Template, User } from 'types'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; @@ -9,6 +7,8 @@ import { authService, emailService } from 'services'; import config from 'config'; +import { AppKoaContext, AppRouter, Next, Template, User } from 'types'; + const schema = z.object({ token: z.string().min(1, 'Token is required'), }); @@ -33,18 +33,12 @@ async function validator(ctx: AppKoaContext, next: Next) { async function handler(ctx: AppKoaContext) { const { user } = ctx.validatedData; - await userService.updateOne( - { _id: user._id }, - () => ({ - isEmailVerified: true, - signupToken: null, - }), - ); - - await Promise.all([ - userService.updateLastRequest(user._id), - authService.setTokens(ctx, user._id), - ]); + await userService.updateOne({ _id: user._id }, () => ({ + isEmailVerified: true, + signupToken: null, + })); + + await Promise.all([userService.updateLastRequest(user._id), authService.setTokens(ctx, user._id)]); await emailService.sendTemplate({ to: user.email, diff --git a/template/apps/api/src/resources/account/actions/verify-reset-token.ts b/template/apps/api/src/resources/account/actions/verify-reset-token.ts index f522d048..f5fcc6ec 100644 --- a/template/apps/api/src/resources/account/actions/verify-reset-token.ts +++ b/template/apps/api/src/resources/account/actions/verify-reset-token.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; -import { AppKoaContext, AppRouter, User } from 'types'; -import { EMAIL_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; import config from 'config'; +import { EMAIL_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, User } from 'types'; + const schema = z.object({ email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), token: z.string().min(1, 'Token is required'), diff --git a/template/apps/api/src/resources/account/index.ts b/template/apps/api/src/resources/account/index.ts index bc8ef17e..ba792e66 100644 --- a/template/apps/api/src/resources/account/index.ts +++ b/template/apps/api/src/resources/account/index.ts @@ -1,5 +1,3 @@ import accountRoutes from './account.routes'; -export { - accountRoutes, -}; +export { accountRoutes }; diff --git a/template/apps/api/src/resources/token/index.ts b/template/apps/api/src/resources/token/index.ts index 4bf69f20..7d6d240b 100644 --- a/template/apps/api/src/resources/token/index.ts +++ b/template/apps/api/src/resources/token/index.ts @@ -1,5 +1,3 @@ import tokenService from './token.service'; -export { - tokenService, -}; +export { tokenService }; diff --git a/template/apps/api/src/resources/token/token.service.ts b/template/apps/api/src/resources/token/token.service.ts index f90d7790..2dd44d42 100644 --- a/template/apps/api/src/resources/token/token.service.ts +++ b/template/apps/api/src/resources/token/token.service.ts @@ -1,11 +1,11 @@ -import { Token, TokenType } from 'types'; -import { tokenSchema } from 'schemas'; -import { DATABASE_DOCUMENTS, TOKEN_SECURITY_LENGTH } from 'app-constants'; - import { securityUtil } from 'utils'; import db from 'db'; +import { DATABASE_DOCUMENTS, TOKEN_SECURITY_LENGTH } from 'app-constants'; +import { tokenSchema } from 'schemas'; +import { Token, TokenType } from 'types'; + const service = db.createService(DATABASE_DOCUMENTS.TOKENS, { schemaValidator: (obj) => tokenSchema.parseAsync(obj), }); @@ -21,10 +21,7 @@ const createToken = async (userId: string, type: TokenType, isShadow?: boolean) }); }; -const createAuthTokens = async ({ - userId, - isShadow, -}: { userId: string, isShadow?: boolean }) => { +const createAuthTokens = async ({ userId, isShadow }: { userId: string; isShadow?: boolean }) => { const accessTokenEntity = await createToken(userId, TokenType.ACCESS, isShadow); return { @@ -35,15 +32,15 @@ const createAuthTokens = async ({ const findTokenByValue = async (token: string) => { const tokenEntity = await service.findOne({ value: token }); - return tokenEntity && { - userId: tokenEntity.userId, - isShadow: tokenEntity.isShadow, - }; + return ( + tokenEntity && { + userId: tokenEntity.userId, + isShadow: tokenEntity.isShadow, + } + ); }; -const removeAuthTokens = async (accessToken: string) => { - return service.deleteMany({ value: { $in: [accessToken] } }); -}; +const removeAuthTokens = async (accessToken: string) => service.deleteMany({ value: { $in: [accessToken] } }); export default Object.assign(service, { createAuthTokens, diff --git a/template/apps/api/src/resources/user/actions/list.ts b/template/apps/api/src/resources/user/actions/list.ts index 30f7592c..9ea5a3f4 100644 --- a/template/apps/api/src/resources/user/actions/list.ts +++ b/template/apps/api/src/resources/user/actions/list.ts @@ -1,22 +1,24 @@ import { z } from 'zod'; -import { AppKoaContext, AppRouter, NestedKeys, User } from 'types'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; - import { stringUtil } from 'utils'; import { paginationSchema } from 'schemas'; +import { AppKoaContext, AppRouter, NestedKeys, User } from 'types'; const schema = paginationSchema.extend({ - filter: z.object({ - createdOn: z.object({ - startDate: z.coerce.date().optional(), - endDate: z.coerce.date().optional(), - }).optional(), - }).optional(), + filter: z + .object({ + createdOn: z + .object({ + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), + }) + .optional(), + }) + .optional(), }); type ValidatedData = z.infer; @@ -44,8 +46,8 @@ async function handler(ctx: AppKoaContext) { filterOptions.push({ createdOn: { - ...(startDate && ({ $gte: startDate })), - ...(endDate && ({ $lt: endDate })), + ...(startDate && { $gte: startDate }), + ...(endDate && { $lt: endDate }), }, }); } diff --git a/template/apps/api/src/resources/user/actions/remove.ts b/template/apps/api/src/resources/user/actions/remove.ts index 67251602..154e57fe 100644 --- a/template/apps/api/src/resources/user/actions/remove.ts +++ b/template/apps/api/src/resources/user/actions/remove.ts @@ -1,7 +1,7 @@ -import { AppKoaContext, AppRouter, Next } from 'types'; - import { userService } from 'resources/user'; +import { AppKoaContext, AppRouter, Next } from 'types'; + type ValidatedData = never; type Request = { params: { diff --git a/template/apps/api/src/resources/user/actions/update.ts b/template/apps/api/src/resources/user/actions/update.ts index 41e6b1bf..09730f14 100644 --- a/template/apps/api/src/resources/user/actions/update.ts +++ b/template/apps/api/src/resources/user/actions/update.ts @@ -1,12 +1,12 @@ import { z } from 'zod'; -import { AppKoaContext, Next, AppRouter } from 'types'; -import { EMAIL_REGEX } from 'app-constants'; - import { userService } from 'resources/user'; import { validateMiddleware } from 'middlewares'; +import { EMAIL_REGEX } from 'app-constants'; +import { AppKoaContext, AppRouter, Next } from 'types'; + const schema = z.object({ firstName: z.string().min(1, 'Please enter First name').max(100), lastName: z.string().min(1, 'Please enter Last name').max(100), @@ -17,7 +17,7 @@ type ValidatedData = z.infer; type Request = { params: { id: string; - } + }; }; async function validator(ctx: AppKoaContext, next: Next) { @@ -31,10 +31,11 @@ async function validator(ctx: AppKoaContext, next: Next) async function handler(ctx: AppKoaContext) { const { firstName, lastName, email } = ctx.validatedData; - const updatedUser = await userService.updateOne( - { _id: ctx.request.params?.id }, - () => ({ firstName, lastName, email }), - ); + const updatedUser = await userService.updateOne({ _id: ctx.request.params?.id }, () => ({ + firstName, + lastName, + email, + })); ctx.body = userService.getPublic(updatedUser); } diff --git a/template/apps/api/src/resources/user/index.ts b/template/apps/api/src/resources/user/index.ts index 93f7dcf0..7ea3b1c0 100644 --- a/template/apps/api/src/resources/user/index.ts +++ b/template/apps/api/src/resources/user/index.ts @@ -1,9 +1,6 @@ -import userService from './user.service'; import userRoutes from './user.routes'; +import userService from './user.service'; import './user.handler'; -export { - userService, - userRoutes, -}; +export { userRoutes, userService }; diff --git a/template/apps/api/src/resources/user/tests/user.service.spec.ts b/template/apps/api/src/resources/user/tests/user.service.spec.ts index 005a5fdd..f74df1f1 100644 --- a/template/apps/api/src/resources/user/tests/user.service.spec.ts +++ b/template/apps/api/src/resources/user/tests/user.service.spec.ts @@ -1,8 +1,8 @@ import { Database } from '@paralect/node-mongo'; -import { User } from 'types'; -import { userSchema } from 'schemas'; import { DATABASE_DOCUMENTS } from 'app-constants'; +import { userSchema } from 'schemas'; +import { User } from 'types'; const database = new Database(process.env.MONGO_URL as string); diff --git a/template/apps/api/src/resources/user/user.handler.ts b/template/apps/api/src/resources/user/user.handler.ts index 6ce0163b..1efef15f 100644 --- a/template/apps/api/src/resources/user/user.handler.ts +++ b/template/apps/api/src/resources/user/user.handler.ts @@ -1,12 +1,11 @@ import { eventBus, InMemoryEvent } from '@paralect/node-mongo'; +import ioEmitter from 'io-emitter'; +import logger from 'logger'; -import { User } from 'types'; import { DATABASE_DOCUMENTS } from 'app-constants'; +import { User } from 'types'; -import logger from 'logger'; -import ioEmitter from 'io-emitter'; - -import { userService } from './index'; +import userService from './user.service'; const { USERS } = DATABASE_DOCUMENTS; @@ -25,10 +24,7 @@ eventBus.onUpdated(USERS, ['firstName', 'lastName'], async (data: InMemoryEvent< const user = data.doc; const fullName = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName; - await userService.atomic.updateOne( - { _id: user._id }, - { $set: { fullName } }, - ); + await userService.atomic.updateOne({ _id: user._id }, { $set: { fullName } }); } catch (err) { logger.error(`${USERS} onUpdated ['firstName', 'lastName'] handler error: ${err}`); } diff --git a/template/apps/api/src/resources/user/user.routes.ts b/template/apps/api/src/resources/user/user.routes.ts index 6c65848d..36504e19 100644 --- a/template/apps/api/src/resources/user/user.routes.ts +++ b/template/apps/api/src/resources/user/user.routes.ts @@ -1,22 +1,14 @@ import { routeUtil } from 'utils'; import list from './actions/list'; -import update from './actions/update'; import remove from './actions/remove'; +import update from './actions/update'; -const publicRoutes = routeUtil.getRoutes([ - -]); +const publicRoutes = routeUtil.getRoutes([]); -const privateRoutes = routeUtil.getRoutes([ - list, -]); +const privateRoutes = routeUtil.getRoutes([list]); -const adminRoutes = routeUtil.getRoutes([ - list, - update, - remove, -]); +const adminRoutes = routeUtil.getRoutes([list, update, remove]); export default { publicRoutes, diff --git a/template/apps/api/src/resources/user/user.service.ts b/template/apps/api/src/resources/user/user.service.ts index 35dfe0d5..6768f412 100644 --- a/template/apps/api/src/resources/user/user.service.ts +++ b/template/apps/api/src/resources/user/user.service.ts @@ -1,17 +1,17 @@ import _ from 'lodash'; -import { User } from 'types'; -import { userSchema } from 'schemas'; -import { DATABASE_DOCUMENTS } from 'app-constants'; - import db from 'db'; +import { DATABASE_DOCUMENTS } from 'app-constants'; +import { userSchema } from 'schemas'; +import { User } from 'types'; + const service = db.createService(DATABASE_DOCUMENTS.USERS, { schemaValidator: (obj) => userSchema.parseAsync(obj), }); -const updateLastRequest = (_id: string) => { - return service.atomic.updateOne( +const updateLastRequest = (_id: string) => + service.atomic.updateOne( { _id }, { $set: { @@ -19,13 +19,8 @@ const updateLastRequest = (_id: string) => { }, }, ); -}; -const privateFields = [ - 'passwordHash', - 'signupToken', - 'resetPasswordToken', -]; +const privateFields = ['passwordHash', 'signupToken', 'resetPasswordToken']; const getPublic = (user: User | null) => _.omit(user, privateFields); diff --git a/template/apps/api/src/routes/admin.routes.ts b/template/apps/api/src/routes/admin.routes.ts index 3ab2b6da..3a5b77be 100644 --- a/template/apps/api/src/routes/admin.routes.ts +++ b/template/apps/api/src/routes/admin.routes.ts @@ -1,11 +1,11 @@ -import mount from 'koa-mount'; import compose from 'koa-compose'; - -import { AppKoa } from 'types'; +import mount from 'koa-mount'; import { accountRoutes } from 'resources/account'; import { userRoutes } from 'resources/user'; +import { AppKoa } from 'types'; + import adminAuth from './middlewares/admin-auth.middleware'; export default (app: AppKoa) => { diff --git a/template/apps/api/src/routes/index.ts b/template/apps/api/src/routes/index.ts index cb14d975..8f71b12d 100644 --- a/template/apps/api/src/routes/index.ts +++ b/template/apps/api/src/routes/index.ts @@ -1,13 +1,13 @@ import { AppKoa } from 'types'; -import tryToAttachUser from './middlewares/try-to-attach-user.middleware'; -import extractTokens from './middlewares/extract-tokens.middleware'; import attachCustomErrors from './middlewares/attach-custom-errors.middleware'; import attachCustomProperties from './middlewares/attach-custom-properties.middleware'; +import extractTokens from './middlewares/extract-tokens.middleware'; import routeErrorHandler from './middlewares/route-error-handler.middleware'; -import publicRoutes from './public.routes'; -import privateRoutes from './private.routes'; +import tryToAttachUser from './middlewares/try-to-attach-user.middleware'; import adminRoutes from './admin.routes'; +import privateRoutes from './private.routes'; +import publicRoutes from './public.routes'; const defineRoutes = (app: AppKoa) => { app.use(attachCustomErrors); @@ -15,7 +15,7 @@ const defineRoutes = (app: AppKoa) => { app.use(routeErrorHandler); app.use(extractTokens); app.use(tryToAttachUser); - + publicRoutes(app); privateRoutes(app); adminRoutes(app); diff --git a/template/apps/api/src/routes/middlewares/admin-auth.middleware.ts b/template/apps/api/src/routes/middlewares/admin-auth.middleware.ts index f27aa336..f4f7b895 100644 --- a/template/apps/api/src/routes/middlewares/admin-auth.middleware.ts +++ b/template/apps/api/src/routes/middlewares/admin-auth.middleware.ts @@ -1,7 +1,7 @@ -import { AppKoaContext, Next } from 'types'; - import config from 'config'; +import { AppKoaContext, Next } from 'types'; + const adminAuth = (ctx: AppKoaContext, next: Next) => { const adminKey = ctx.header['x-admin-key']; diff --git a/template/apps/api/src/routes/middlewares/attach-custom-errors.middleware.ts b/template/apps/api/src/routes/middlewares/attach-custom-errors.middleware.ts index cf906c61..57f17e75 100644 --- a/template/apps/api/src/routes/middlewares/attach-custom-errors.middleware.ts +++ b/template/apps/api/src/routes/middlewares/attach-custom-errors.middleware.ts @@ -1,20 +1,21 @@ import _ from 'lodash'; -import { ValidationErrors, AppKoaContext, Next, CustomErrors } from 'types'; +import { AppKoaContext, CustomErrors, Next, ValidationErrors } from 'types'; const formatError = (customError: CustomErrors): ValidationErrors => { const errors: ValidationErrors = {}; Object.keys(customError).forEach((key) => { - errors[key] = _.isArray(customError[key]) - ? customError[key] - : [customError[key]]; + errors[key] = _.isArray(customError[key]) ? customError[key] : [customError[key]]; }); return errors; }; -const attachCustomErrors = async (ctx: AppKoaContext, next: Next) => { +const attachCustomErrors = async (ctx: AppKoaContext<{ db: number }>, next: Next) => { + const db = 1; + + ctx.validatedData.db = db; ctx.throwError = (message, status = 400) => ctx.throw(status, { message }); ctx.assertError = (condition, message, status = 400) => ctx.assert(condition, status, { message }); diff --git a/template/apps/api/src/routes/middlewares/extract-tokens.middleware.ts b/template/apps/api/src/routes/middlewares/extract-tokens.middleware.ts index 76fed144..cdb94b9b 100644 --- a/template/apps/api/src/routes/middlewares/extract-tokens.middleware.ts +++ b/template/apps/api/src/routes/middlewares/extract-tokens.middleware.ts @@ -1,5 +1,5 @@ -import { AppKoaContext, Next } from 'types'; import { COOKIES } from 'app-constants'; +import { AppKoaContext, Next } from 'types'; const storeTokenToState = async (ctx: AppKoaContext, next: Next) => { let accessToken = ctx.cookies.get(COOKIES.ACCESS_TOKEN); diff --git a/template/apps/api/src/routes/middlewares/route-error-handler.middleware.ts b/template/apps/api/src/routes/middlewares/route-error-handler.middleware.ts index 72519682..4a6a0d9a 100644 --- a/template/apps/api/src/routes/middlewares/route-error-handler.middleware.ts +++ b/template/apps/api/src/routes/middlewares/route-error-handler.middleware.ts @@ -1,10 +1,11 @@ -import { AppKoaContext, Next, ValidationErrors } from 'types'; +import logger from 'logger'; import { userService } from 'resources/user'; -import logger from 'logger'; import config from 'config'; +import { AppKoaContext, Next, ValidationErrors } from 'types'; + interface CustomError extends Error { status?: number; clientErrors?: ValidationErrors; diff --git a/template/apps/api/src/routes/middlewares/try-to-attach-user.middleware.ts b/template/apps/api/src/routes/middlewares/try-to-attach-user.middleware.ts index 873bd47d..2bbc65be 100644 --- a/template/apps/api/src/routes/middlewares/try-to-attach-user.middleware.ts +++ b/template/apps/api/src/routes/middlewares/try-to-attach-user.middleware.ts @@ -1,10 +1,10 @@ -import { AppKoaContext, Next } from 'types'; - -import { userService } from 'resources/user'; import { tokenService } from 'resources/token'; +import { userService } from 'resources/user'; + +import { AppKoaContext, Next } from 'types'; const tryToAttachUser = async (ctx: AppKoaContext, next: Next) => { - const accessToken = ctx.state.accessToken; + const { accessToken } = ctx.state; let userData; if (accessToken) { diff --git a/template/apps/api/src/routes/private.routes.ts b/template/apps/api/src/routes/private.routes.ts index 34d2a908..39d09c5d 100644 --- a/template/apps/api/src/routes/private.routes.ts +++ b/template/apps/api/src/routes/private.routes.ts @@ -1,11 +1,11 @@ -import mount from 'koa-mount'; import compose from 'koa-compose'; - -import { AppKoa } from 'types'; +import mount from 'koa-mount'; import { accountRoutes } from 'resources/account'; import { userRoutes } from 'resources/user'; +import { AppKoa } from 'types'; + import auth from './middlewares/auth.middleware'; export default (app: AppKoa) => { diff --git a/template/apps/api/src/routes/public.routes.ts b/template/apps/api/src/routes/public.routes.ts index b049182f..e8639188 100644 --- a/template/apps/api/src/routes/public.routes.ts +++ b/template/apps/api/src/routes/public.routes.ts @@ -1,11 +1,13 @@ import mount from 'koa-mount'; -import { AppKoa, AppRouter } from 'types'; - import { accountRoutes } from 'resources/account'; +import { AppKoa, AppRouter } from 'types'; + const healthCheckRouter = new AppRouter(); -healthCheckRouter.get('/health', ctx => ctx.status = 200); +healthCheckRouter.get('/health', (ctx) => { + ctx.status = 200; +}); export default (app: AppKoa) => { app.use(healthCheckRouter.routes()); diff --git a/template/apps/api/src/scheduler.ts b/template/apps/api/src/scheduler.ts index 07ad0d7c..a165756c 100644 --- a/template/apps/api/src/scheduler.ts +++ b/template/apps/api/src/scheduler.ts @@ -1,15 +1,14 @@ // allows to require modules relative to /src folder // for example: require('lib/mongo/idGenerator') // all options can be found here: https://gist.github.com/branneman/8048520 -import moduleAlias from 'module-alias'; -moduleAlias.addPath(__dirname); -moduleAlias(); // read aliases from package json - -import 'dotenv/config'; - import logger from 'logger'; +import moduleAlias from 'module-alias'; // read aliases from package json +import 'dotenv/config'; import 'scheduler/cron'; import 'scheduler/handlers/action.example.handler'; +moduleAlias.addPath(__dirname); +moduleAlias(); + logger.info('[Scheduler] Server has been started'); diff --git a/template/apps/api/src/scheduler/cron/index.ts b/template/apps/api/src/scheduler/cron/index.ts index c7bab326..18c48037 100644 --- a/template/apps/api/src/scheduler/cron/index.ts +++ b/template/apps/api/src/scheduler/cron/index.ts @@ -1,5 +1,5 @@ -import schedule from 'node-schedule'; import { EventEmitter } from 'events'; +import schedule from 'node-schedule'; const eventEmitter = new EventEmitter(); diff --git a/template/apps/api/src/scheduler/handlers/action.example.handler.ts b/template/apps/api/src/scheduler/handlers/action.example.handler.ts index 6f79c2d5..5a15430c 100644 --- a/template/apps/api/src/scheduler/handlers/action.example.handler.ts +++ b/template/apps/api/src/scheduler/handlers/action.example.handler.ts @@ -1,6 +1,7 @@ +import logger from 'logger'; + import cron from 'scheduler/cron'; -import logger from 'logger'; import config from 'config'; const schedule = { @@ -11,6 +12,7 @@ const schedule = { cron.on(schedule[config.APP_ENV], async () => { try { + // Scheduler logic } catch (error) { logger.error(error); } diff --git a/template/apps/api/src/services/analytics/analytics.service.ts b/template/apps/api/src/services/analytics/analytics.service.ts index 728d9d86..9af74a4b 100644 --- a/template/apps/api/src/services/analytics/analytics.service.ts +++ b/template/apps/api/src/services/analytics/analytics.service.ts @@ -1,11 +1,9 @@ +import logger from 'logger'; import Mixpanel from 'mixpanel'; import config from 'config'; -import logger from 'logger'; -const mixpanel = config.MIXPANEL_API_KEY - ? Mixpanel.init(config.MIXPANEL_API_KEY, { debug: config.IS_DEV }) - : null; +const mixpanel = config.MIXPANEL_API_KEY ? Mixpanel.init(config.MIXPANEL_API_KEY, { debug: config.IS_DEV }) : null; const track = (event: string, data = {}) => { if (!mixpanel) { diff --git a/template/apps/api/src/services/auth/auth.helper.ts b/template/apps/api/src/services/auth/auth.helper.ts index 71e68bdb..44434094 100644 --- a/template/apps/api/src/services/auth/auth.helper.ts +++ b/template/apps/api/src/services/auth/auth.helper.ts @@ -1,15 +1,12 @@ import psl from 'psl'; import url from 'url'; -import { AppKoaContext } from 'types'; -import { COOKIES } from 'app-constants'; - import config from 'config'; -export const setTokenCookies = ({ - ctx, - accessToken, -}: { ctx: AppKoaContext, accessToken: string }) => { +import { COOKIES } from 'app-constants'; +import { AppKoaContext } from 'types'; + +export const setTokenCookies = ({ ctx, accessToken }: { ctx: AppKoaContext; accessToken: string }) => { const parsedUrl = url.parse(config.WEB_URL); if (!parsedUrl.hostname) { diff --git a/template/apps/api/src/services/auth/auth.service.ts b/template/apps/api/src/services/auth/auth.service.ts index f2e20192..4a8041a4 100644 --- a/template/apps/api/src/services/auth/auth.service.ts +++ b/template/apps/api/src/services/auth/auth.service.ts @@ -1,6 +1,7 @@ -import { AppKoaContext } from 'types'; import { tokenService } from 'resources/token'; +import { AppKoaContext } from 'types'; + import cookieHelper from './auth.helper'; const setTokens = async (ctx: AppKoaContext, userId: string, isShadow?: boolean) => { diff --git a/template/apps/api/src/services/cloud-storage/cloud-storage.helper.ts b/template/apps/api/src/services/cloud-storage/cloud-storage.helper.ts index 69d5ae20..18a89038 100644 --- a/template/apps/api/src/services/cloud-storage/cloud-storage.helper.ts +++ b/template/apps/api/src/services/cloud-storage/cloud-storage.helper.ts @@ -1,4 +1,6 @@ -export const getFileKey = (url: string) => { +export const getFileKey = (url: string | null | undefined) => { + if (!url) return ''; + const decodedUrl = decodeURI(url); const { pathname } = new URL(decodedUrl); diff --git a/template/apps/api/src/services/cloud-storage/cloud-storage.service.ts b/template/apps/api/src/services/cloud-storage/cloud-storage.service.ts index e8f40636..253ec234 100644 --- a/template/apps/api/src/services/cloud-storage/cloud-storage.service.ts +++ b/template/apps/api/src/services/cloud-storage/cloud-storage.service.ts @@ -1,14 +1,20 @@ -import { S3Client, GetObjectCommand, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { Upload } from '@aws-sdk/lib-storage'; - -import type { File } from '@koa/multer'; -import type { +import { + CompleteMultipartUploadCommandOutput, + CopyObjectCommand, + CopyObjectCommandOutput, + DeleteObjectCommand, + DeleteObjectCommandOutput, + GetObjectCommand, GetObjectOutput, - CopyObjectOutput, - DeleteObjectOutput, - CompleteMultipartUploadOutput, + S3Client, } from '@aws-sdk/client-s3'; +import { PutObjectCommandInput } from '@aws-sdk/client-s3/dist-types/commands/PutObjectCommand'; +import { Upload } from '@aws-sdk/lib-storage'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import type { File } from '@koa/multer'; +import { ToCamelCase } from 'app-types'; + +import { caseUtil } from 'utils'; import config from 'config'; @@ -23,11 +29,13 @@ const client = new S3Client({ secretAccessKey: config.CLOUD_STORAGE_SECRET_ACCESS_KEY ?? '', }, }); -const Bucket = config.CLOUD_STORAGE_BUCKET; +const bucket = config.CLOUD_STORAGE_BUCKET; -const upload = (fileName: string, file: File): Promise => { - const params = { - Bucket, +type UploadOutput = ToCamelCase; + +const upload = async (fileName: string, file: File): Promise => { + const params: PutObjectCommandInput = { + Bucket: bucket, ContentType: file.mimetype, Body: file.buffer, Key: fileName, @@ -39,12 +47,12 @@ const upload = (fileName: string, file: File): Promise caseUtil.toCamelCase(value)); }; -const uploadPublic = (fileName: string, file: File): Promise => { - const params = { - Bucket, +const uploadPublic = async (fileName: string, file: File): Promise => { + const params: PutObjectCommandInput = { + Bucket: bucket, ContentType: file.mimetype, Body: file.buffer, Key: fileName, @@ -56,12 +64,12 @@ const uploadPublic = (fileName: string, file: File): Promise caseUtil.toCamelCase(value)); }; const getSignedDownloadUrl = (fileName: string): Promise => { const command = new GetObjectCommand({ - Bucket, + Bucket: bucket, Key: fileName, }); @@ -70,38 +78,41 @@ const getSignedDownloadUrl = (fileName: string): Promise => { const getObject = (fileName: string): Promise => { const command = new GetObjectCommand({ - Bucket, + Bucket: bucket, Key: fileName, }); return client.send(command); }; -const copyObject = (filePath: string, copyFilePath: string): Promise => { +type CopyOutput = ToCamelCase; + +const copyObject = async (filePath: string, copyFilePath: string): Promise => { const command = new CopyObjectCommand({ - Bucket, - CopySource: encodeURI(`${Bucket}/${copyFilePath}`), + Bucket: bucket, + CopySource: encodeURI(`${bucket}/${copyFilePath}`), Key: filePath, }); - return client.send(command); + return client.send(command).then((value) => caseUtil.toCamelCase(value)); }; -const deleteObject = (fileName: string): Promise => { - const command = new DeleteObjectCommand( { - Bucket, +type DeleteOutput = ToCamelCase; + +const deleteObject = async (fileName: string): Promise => { + const command = new DeleteObjectCommand({ + Bucket: bucket, Key: fileName, }); - return client.send(command); + return client.send(command).then((value) => caseUtil.toCamelCase(value)); }; -export default { - helpers, +export default Object.assign(helpers, { upload, uploadPublic, getObject, copyObject, deleteObject, getSignedDownloadUrl, -}; +}); diff --git a/template/apps/api/src/services/email/email.service.ts b/template/apps/api/src/services/email/email.service.ts index 885b4692..2cba9faa 100644 --- a/template/apps/api/src/services/email/email.service.ts +++ b/template/apps/api/src/services/email/email.service.ts @@ -1,11 +1,10 @@ -import config from 'config'; import sendgrid from '@sendgrid/mail'; - +import logger from 'logger'; import { renderEmailHtml, Template } from 'mailer'; -import logger from 'logger'; +import config from 'config'; -import { From, EmailServiceConstructorProps, SendTemplateParams, SendSendgridTemplateParams } from './email.types'; +import { EmailServiceConstructorProps, From, SendSendgridTemplateParams, SendTemplateParams } from './email.types'; class EmailService { apiKey: string | undefined; @@ -19,13 +18,7 @@ class EmailService { if (apiKey) sendgrid.setApiKey(apiKey); } - async sendTemplate({ - to, - subject, - template, - params, - attachments, - }: SendTemplateParams) { + async sendTemplate({ to, subject, template, params, attachments }: SendTemplateParams) { if (!this.apiKey) { logger.error('[Sendgrid] API key is not provided'); return null; @@ -33,16 +26,18 @@ class EmailService { const html = await renderEmailHtml({ template, params }); - return sendgrid.send({ - from: this.from, - to, - subject, - html, - attachments, - }).then(() => { - logger.debug(`[Sendgrid] Sent email to ${to}.`); - logger.debug({ subject, template, params }); - }); + return sendgrid + .send({ + from: this.from, + to, + subject, + html, + attachments, + }) + .then(() => { + logger.debug(`[Sendgrid] Sent email to ${to}.`); + logger.debug({ subject, template, params }); + }); } async sendSendgridTemplate({ @@ -57,21 +52,22 @@ class EmailService { return null; } - return sendgrid.send({ - from: this.from, - to, - subject, - templateId, - dynamicTemplateData, - attachments, - }).then(() => { - logger.debug(`[Sendgrid] Sent email to ${to}.`); - logger.debug({ subject, templateId, dynamicTemplateData }); - }); + return sendgrid + .send({ + from: this.from, + to, + subject, + templateId, + dynamicTemplateData, + attachments, + }) + .then(() => { + logger.debug(`[Sendgrid] Sent email to ${to}.`); + logger.debug({ subject, templateId, dynamicTemplateData }); + }); } } - export default new EmailService({ apiKey: config.SENDGRID_API_KEY, from: { diff --git a/template/apps/api/src/services/email/email.types.ts b/template/apps/api/src/services/email/email.types.ts index 29a5ea4f..61bb55c5 100644 --- a/template/apps/api/src/services/email/email.types.ts +++ b/template/apps/api/src/services/email/email.types.ts @@ -1,10 +1,10 @@ import { Template, TemplateProps } from 'mailer'; -export type From = { email: string, name: string }; +export type From = { email: string; name: string }; export interface EmailServiceConstructorProps { - apiKey: string | undefined, - from: From, + apiKey: string | undefined; + from: From; } interface Attachment { @@ -14,17 +14,17 @@ interface Attachment { } export interface SendTemplateParams { - to: string, - subject: string, - template: T, - params: TemplateProps[T], - attachments?: Attachment[], + to: string; + subject: string; + template: T; + params: TemplateProps[T]; + attachments?: Attachment[]; } export interface SendSendgridTemplateParams { - to: string, - subject: string, - templateId: string, - dynamicTemplateData: { [key: string]: unknown }, - attachments?: Attachment[], + to: string; + subject: string; + templateId: string; + dynamicTemplateData: { [key: string]: unknown }; + attachments?: Attachment[]; } diff --git a/template/apps/api/src/services/google/google.service.ts b/template/apps/api/src/services/google/google.service.ts index 2f06fb1b..b102127d 100644 --- a/template/apps/api/src/services/google/google.service.ts +++ b/template/apps/api/src/services/google/google.service.ts @@ -1,7 +1,12 @@ -import { OAuth2Client } from 'google-auth-library'; +import { OAuth2Client, TokenPayload } from 'google-auth-library'; +import _ from 'lodash'; + +import { caseUtil } from 'utils'; import config from 'config'; +import { ToCamelCase } from 'types'; + const client = new OAuth2Client( config.GOOGLE_CLIENT_ID, config.GOOGLE_CLIENT_SECRET, @@ -14,30 +19,43 @@ const oAuthURL = client.generateAuthUrl({ include_granted_scopes: true, }); -const exchangeCodeForToken = async (code: string) => { +type ConvertedPayload = ToCamelCase | undefined; + +type ExchangeResponse = { + isValid: boolean; + payload: ConvertedPayload | Error | null; +}; + +const exchangeCodeForToken = async (code?: string | string[] | undefined): Promise => { + if (!code || _.isArray(code)) { + return { isValid: false, payload: new Error('Code not found') }; + } + try { const { tokens } = await client.getToken(code); - const ticket = await client.verifyIdToken({ - idToken: tokens.id_token || '', + if (!tokens.id_token) { + return { isValid: false, payload: new Error('ID token not found') }; + } + + const loginTicket = await client.verifyIdToken({ + idToken: tokens.id_token, audience: config.GOOGLE_CLIENT_ID, }); - return { - isValid: true, - payload: ticket.getPayload(), - }; - } catch ({ message, ...rest }) { - return { - isValid: false, - payload: { message }, - }; + const payload = caseUtil.toCamelCase(loginTicket.getPayload()); + + return { isValid: true, payload }; + } catch (e) { + if (e instanceof Error) { + return { isValid: false, payload: e }; + } + + return { isValid: false, payload: new Error(`Unknown error: ${e}`) }; } }; - export default { oAuthURL, exchangeCodeForToken, }; - diff --git a/template/apps/api/src/services/rate-limit/rate-limit.service.ts b/template/apps/api/src/services/rate-limit/rate-limit.service.ts index faf40073..e756141d 100644 --- a/template/apps/api/src/services/rate-limit/rate-limit.service.ts +++ b/template/apps/api/src/services/rate-limit/rate-limit.service.ts @@ -1,12 +1,12 @@ -import rateLimit from 'koa-ratelimit'; import { ParameterizedContext } from 'koa'; - +import rateLimit from 'koa-ratelimit'; import redisClient from 'redis-client'; import { AppKoaContextState } from 'types'; const rateLimiter = (limitDuration: number, requestsPerDuration: number): ReturnType => { - const errorMessage = 'Looks like you are moving too fast. Retry again in one minute. Please reach out to support with questions.'; + const errorMessage = + 'Looks like you are moving too fast. Retry again in one minute. Please reach out to support with questions.'; return rateLimit({ driver: 'redis', diff --git a/template/apps/api/src/services/socket/socket.service.ts b/template/apps/api/src/services/socket/socket.service.ts index 4cdcdc1b..b5bbc639 100644 --- a/template/apps/api/src/services/socket/socket.service.ts +++ b/template/apps/api/src/services/socket/socket.service.ts @@ -1,13 +1,12 @@ +import { createAdapter } from '@socket.io/redis-adapter'; import http from 'http'; +import logger from 'logger'; +import pubClient, { redisErrorHandler } from 'redis-client'; import { Server } from 'socket.io'; -import { createAdapter } from '@socket.io/redis-adapter'; - -import { COOKIES } from 'app-constants'; import { tokenService } from 'resources/token'; -import pubClient, { redisErrorHandler } from 'redis-client'; -import logger from 'logger'; +import { COOKIES } from 'app-constants'; import socketHelper from './socket.helper'; diff --git a/template/apps/api/src/types.ts b/template/apps/api/src/types.ts index 44ce4743..5fd0b79e 100644 --- a/template/apps/api/src/types.ts +++ b/template/apps/api/src/types.ts @@ -1,9 +1,7 @@ -import Koa, { ParameterizedContext, Request, Next } from 'koa'; import Router from '@koa/router'; - -import { Template } from 'mailer'; - import { User } from 'app-types'; +import Koa, { Next, ParameterizedContext, Request } from 'koa'; +import { Template } from 'mailer'; export * from 'app-types'; @@ -16,7 +14,7 @@ export type AppKoaContextState = { export type CustomErrors = { [name: string]: string; }; - + export interface AppKoaContext extends ParameterizedContext { request: Request & R; validatedData: T & object; diff --git a/template/apps/api/src/utils/case.util.ts b/template/apps/api/src/utils/case.util.ts new file mode 100644 index 00000000..8c096c2d --- /dev/null +++ b/template/apps/api/src/utils/case.util.ts @@ -0,0 +1,24 @@ +import { camelCase, isArray, isObject, transform } from 'lodash'; + +type NonNullableObject = Record; + +export const toCamelCase = (object: T): T => { + if (object === null || object === undefined) { + return object; + } + + const transformObject = (input: NonNullableObject): NonNullableObject => + transform(input, (result: NonNullableObject, value, key) => { + const camelKey = camelCase(key); + + if (isObject(value) && !isArray(value)) { + result[camelKey] = transformObject(value as NonNullableObject); + } else if (isArray(value)) { + result[camelKey] = value.map((item) => (isObject(item) ? transformObject(item as NonNullableObject) : item)); + } else { + result[camelKey] = value; + } + }); + + return isObject(object) && !isArray(object) ? (transformObject(object as NonNullableObject) as T) : object; +}; diff --git a/template/apps/api/src/utils/config.util.ts b/template/apps/api/src/utils/config.util.ts index 87f7a305..3a3c46ae 100644 --- a/template/apps/api/src/utils/config.util.ts +++ b/template/apps/api/src/utils/config.util.ts @@ -4,6 +4,8 @@ const validateConfig = (schema: ZodSchema): T => { const parsed = schema.safeParse(process.env); if (!parsed.success) { + // Allow the use of a console instance for logging before launching the application. + // eslint-disable-next-line no-console console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors); throw new Error('Invalid environment variables'); diff --git a/template/apps/api/src/utils/index.ts b/template/apps/api/src/utils/index.ts index 225d6e64..f4b3c71d 100644 --- a/template/apps/api/src/utils/index.ts +++ b/template/apps/api/src/utils/index.ts @@ -1,13 +1,8 @@ +import * as caseUtil from './case.util'; import configUtil from './config.util'; import promiseUtil from './promise.util'; import routeUtil from './routes.util'; import securityUtil from './security.util'; import stringUtil from './string.util'; -export { - configUtil, - promiseUtil, - routeUtil, - securityUtil, - stringUtil, -}; +export { caseUtil, configUtil, promiseUtil, routeUtil, securityUtil, stringUtil }; diff --git a/template/apps/api/src/utils/promise.util.ts b/template/apps/api/src/utils/promise.util.ts index f9a34a84..1f3b6614 100644 --- a/template/apps/api/src/utils/promise.util.ts +++ b/template/apps/api/src/utils/promise.util.ts @@ -1,10 +1,6 @@ import _ from 'lodash'; -const promiseLimit = ( - documents: T[], - limit: number, - operator: (document: T) => Promise, -): Promise => { +const promiseLimit = (documents: T[], limit: number, operator: (document: T) => Promise): Promise => { const chunks = _.chunk(documents, limit); return chunks.reduce>(async (previousPromise, chunk) => { diff --git a/template/apps/api/src/utils/security.util.ts b/template/apps/api/src/utils/security.util.ts index 3406b1ac..a7113b01 100644 --- a/template/apps/api/src/utils/security.util.ts +++ b/template/apps/api/src/utils/security.util.ts @@ -1,5 +1,5 @@ -import crypto from 'crypto'; import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; /** * @desc Generates random string, useful for creating secure tokens @@ -18,9 +18,7 @@ export const generateSecureToken = async (tokenLength = 48) => { * @param text {string} - a text to produce hash from * @return {Promise} - a hash from input text */ -export const getHash = (text: string) => { - return bcrypt.hash(text, 10); -}; +export const getHash = (text: string) => bcrypt.hash(text, 10); /** * @desc Compares if text and hash are equal @@ -29,9 +27,7 @@ export const getHash = (text: string) => { * @param hash {string} - a hash to compare with text * @return {Promise} - are hash and text equal */ -export const compareTextWithHash = (text: string, hash: string) => { - return bcrypt.compare(text, hash); -}; +export const compareTextWithHash = (text: string, hash: string) => bcrypt.compare(text, hash); export default { generateSecureToken, diff --git a/template/apps/api/tsconfig.json b/template/apps/api/tsconfig.json index 34ba82c3..3a700156 100644 --- a/template/apps/api/tsconfig.json +++ b/template/apps/api/tsconfig.json @@ -1,28 +1,9 @@ { + "extends": "tsconfig/nodejs.json", "compilerOptions": { - "baseUrl": "src", "rootDir": ".", - "target": "es2017", - "lib": ["es6"], - "module": "commonjs", - "moduleResolution": "node", - "paths": { "*": ["*"] }, - "sourceMap": true, - "outDir": "dist", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "useUnknownInCatchVariables": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "resolveJsonModule": true + "baseUrl": "src", + "jsx": "preserve" }, - "include": [ - "src/**/*", - "src/**/*.json", - ".eslintrc.js" - ], - "ts-node": { "transpileOnly": true }, - "exclude": [] + "include": ["**/*.ts", "**/*.json"] } diff --git a/template/apps/web/.dockerignore b/template/apps/web/.dockerignore index 387b8cb5..15989c8c 100644 --- a/template/apps/web/.dockerignore +++ b/template/apps/web/.dockerignore @@ -1,4 +1,33 @@ -node_modules -.idea -.husky -.storybook +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/template/apps/web/.eslintignore b/template/apps/web/.eslintignore new file mode 100644 index 00000000..15989c8c --- /dev/null +++ b/template/apps/web/.eslintignore @@ -0,0 +1,33 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/template/apps/web/.eslintrc.js b/template/apps/web/.eslintrc.js index 7ff7004a..edff7e22 100644 --- a/template/apps/web/.eslintrc.js +++ b/template/apps/web/.eslintrc.js @@ -1,69 +1,4 @@ module.exports = { root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'tsc'], - env: { - browser: true, - es2021: true, - }, - extends: [ - 'next', - 'airbnb', - 'airbnb-typescript', - 'plugin:storybook/recommended' - ], - ignorePatterns: ['.eslintrc.js', '!.storybook'], - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - ecmaVersion: 13, - sourceType: 'module', - }, - rules: { - 'tsc/config': [2, { - configFile: 'tsconfig.json' - }], - // solve problem with public folder - 'import/no-unresolved': [2, - { ignore: ['public'] }, - ], - 'react/prop-types': 'off', - 'react/jsx-key': 'off', - 'react/require-default-props': 'off', - 'import/prefer-default-export': 'off', - 'import/no-anonymous-default-export': 'off', - 'no-underscore-dangle': 'off', - 'jsx-a11y/label-has-associated-control': 'off', - 'jsx-a11y/anchor-is-valid': 'off', - 'react/display-name': 'off', - 'react/react-in-jsx-scope': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/no-unstable-nested-components': 'off', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'error', - 'object-curly-newline': 'off', - 'no-restricted-imports': ['error', { - name: 'lodash', - message: 'Import individual methods from the Lodash module', - }], - 'max-classes-per-file': 'off', - 'no-proto': 'off', - 'consistent-return': 'off', - 'import/no-extraneous-dependencies': 'off', - '@next/next/no-img-element': 'off', - 'react/function-component-definition': [2, { - namedComponents: 'arrow-function', - }], - }, - settings: { - 'import/resolver': { - node: { - extensions: ['.ts', '.tsx'], - paths: [ - 'src', - 'node_modules', - ], - }, - }, - }, + extends: ['custom/next'], }; diff --git a/template/apps/web/.gitignore b/template/apps/web/.gitignore index f00f3311..15989c8c 100644 --- a/template/apps/web/.gitignore +++ b/template/apps/web/.gitignore @@ -14,22 +14,20 @@ /build # misc -.idea -.vscode .DS_Store *.pem -tsconfig.tsbuildinfo # debug npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local # vercel .vercel -.env -.env.local - -#storybook -storybook-static +# typescript +*.tsbuildinfo diff --git a/template/apps/web/.prettierignore b/template/apps/web/.prettierignore new file mode 100644 index 00000000..15989c8c --- /dev/null +++ b/template/apps/web/.prettierignore @@ -0,0 +1,33 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/template/apps/web/.prettierrc.json b/template/apps/web/.prettierrc.json new file mode 100644 index 00000000..ba63a7c6 --- /dev/null +++ b/template/apps/web/.prettierrc.json @@ -0,0 +1 @@ +"prettier-config-custom" diff --git a/template/apps/web/.storybook/preview.tsx b/template/apps/web/.storybook/preview.tsx index 83cac067..9638cb46 100644 --- a/template/apps/web/.storybook/preview.tsx +++ b/template/apps/web/.storybook/preview.tsx @@ -16,11 +16,11 @@ const ColorSchemeWrapper = ({ children }: { children: React.ReactNode }) => { useEffect(() => { channel.on(DARK_MODE_EVENT_NAME, handleColorScheme); return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [channel]); // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{ children }; + return <>{children}; }; export const decorators = [ diff --git a/template/apps/web/next.config.js b/template/apps/web/next.config.js index 289dd169..4c7cd060 100644 --- a/template/apps/web/next.config.js +++ b/template/apps/web/next.config.js @@ -6,6 +6,7 @@ const dotenvConfig = dotenv.config({ silent: true, }); +/** @type {import('next').NextConfig} */ module.exports = { env: dotenvConfig.parsed, webpack(config) { @@ -16,18 +17,12 @@ module.exports = { return config; }, + reactStrictMode: true, output: 'standalone', experimental: { // this includes files from the monorepo base two directories up outputFileTracingRoot: join(__dirname, '../../'), }, pageExtensions: ['page.tsx', 'api.ts'], - reactStrictMode: true, - typescript: { - ignoreBuildErrors: true, - }, - eslint: { - ignoreDuringBuilds: true, - }, - transpilePackages: ['types', 'schemas', 'app-constants'], + transpilePackages: ['app-constants', 'schemas', 'types'], }; diff --git a/template/apps/web/package.json b/template/apps/web/package.json index 656b4662..94b35cbc 100644 --- a/template/apps/web/package.json +++ b/template/apps/web/package.json @@ -54,7 +54,6 @@ "@storybook/preview-api": "7.6.14", "@storybook/react": "7.6.14", "@storybook/test": "7.6.14", - "@tanstack/eslint-plugin-query": "5.20.1", "@tanstack/react-query-devtools": "5.20.1", "@types/lodash": "4.14.202", "@types/mixpanel-browser": "2.49.0", @@ -62,19 +61,17 @@ "@types/qs": "6.9.11", "@types/react": "18.2.55", "@types/react-dom": "18.2.19", - "@typescript-eslint/eslint-plugin": "6.21.0", - "@typescript-eslint/parser": "6.21.0", "eslint": "8.56.0", - "eslint-config-airbnb": "19.0.4", - "eslint-config-airbnb-typescript": "17.1.0", - "eslint-config-next": "14.1.0", - "eslint-plugin-storybook": "0.6.15", + "eslint-config-custom": "workspace:*", "lint-staged": "15.2.2", "postcss": "8.4.35", "postcss-preset-mantine": "1.13.0", "postcss-simple-vars": "7.0.1", + "prettier": "3.2.5", + "prettier-config-custom": "workspace:*", "storybook": "7.6.14", "storybook-dark-mode": "3.0.3", + "tsconfig": "workspace:*", "typescript": "5.3.3" }, "lint-staged": { diff --git a/template/apps/web/src/components/Table/index.tsx b/template/apps/web/src/components/Table/index.tsx index 17b7a808..78837d7f 100644 --- a/template/apps/web/src/components/Table/index.tsx +++ b/template/apps/web/src/components/Table/index.tsx @@ -1,12 +1,5 @@ -import { useMemo, useCallback, useState, FC } from 'react'; -import { - Table as TableContainer, - Checkbox, - Pagination, - Group, - Text, - Paper, -} from '@mantine/core'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { Checkbox, Group, Pagination, Paper, Table as TableContainer, Text } from '@mantine/core'; import { ColumnDef, flexRender, @@ -20,8 +13,8 @@ import { useReactTable, } from '@tanstack/react-table'; -import Thead from './thead'; import Tbody from './tbody'; +import Thead from './thead'; type SpacingSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; @@ -40,6 +33,26 @@ interface TableProps { page?: number; } +const selectableColumns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, +]; + const Table: FC = ({ data, dataCount, @@ -61,36 +74,24 @@ const Table: FC = ({ const isSelectable = !!rowSelection && !!setRowSelection; const isSortable = useMemo(() => !!onSortingChange, [onSortingChange]); - const selectableColumns: ColumnDef[] = useMemo(() => [{ - id: 'select', - header: ({ table }) => ( - - ), - cell: ({ row }) => ( - - ), - }], []); - - const pagination = useMemo(() => ({ - pageIndex, - pageSize, - }), [pageIndex, pageSize]); + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize], + ); - const onPageChangeHandler = useCallback((currentPage: any, direction?: string) => { - setPagination({ pageIndex: currentPage, pageSize }); + const onPageChangeHandler = useCallback( + (currentPage: any, direction?: string) => { + setPagination({ pageIndex: currentPage, pageSize }); - if (onPageChange) { - onPageChange((prev: Record) => ({ ...prev, page: currentPage, direction })); - } - }, [onPageChange, pageSize]); + if (onPageChange) { + onPageChange((prev: Record) => ({ ...prev, page: currentPage, direction })); + } + }, + [onPageChange, pageSize], + ); const table = useReactTable({ data, @@ -113,48 +114,22 @@ const Table: FC = ({ // eslint-disable-next-line @typescript-eslint/no-shadow const { pageIndex } = table.getState().pagination; - return ( - - ); + return ; }, [onPageChangeHandler, table]); return ( <> - - - + + + {dataCount && ( - Showing - {' '} - {table.getRowModel().rows.length} - {' '} - of - {' '} - {dataCount} - {' '} - results + Showing {table.getRowModel().rows.length} of {dataCount} results )} {renderPagination()} diff --git a/template/apps/web/src/components/Table/tbody/index.tsx b/template/apps/web/src/components/Table/tbody/index.tsx index 242a9da1..5b090505 100644 --- a/template/apps/web/src/components/Table/tbody/index.tsx +++ b/template/apps/web/src/components/Table/tbody/index.tsx @@ -1,17 +1,17 @@ -import { FC, ReactNode } from 'react'; -import { CellContext, ColumnDefTemplate, Row } from '@tanstack/react-table'; +import React, { FC, ReactNode } from 'react'; import { Table, useMantineTheme } from '@mantine/core'; +import { CellContext, ColumnDefTemplate, Row } from '@tanstack/react-table'; type RowData = { [key: string]: string | number | boolean | Record; }; interface TbodyProps { - isSelectable: boolean, + isSelectable: boolean; rows: Row[]; flexRender: ( template: ColumnDefTemplate> | undefined, - context: CellContext + context: CellContext, ) => ReactNode; } @@ -24,16 +24,15 @@ const Tbody: FC = ({ isSelectable, rows, flexRender }) => { {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))} diff --git a/template/apps/web/src/components/Table/thead/index.tsx b/template/apps/web/src/components/Table/thead/index.tsx index 46eb5c5b..65b4deb2 100644 --- a/template/apps/web/src/components/Table/thead/index.tsx +++ b/template/apps/web/src/components/Table/thead/index.tsx @@ -1,24 +1,20 @@ -import { FC, ReactNode } from 'react'; +import React, { FC, ReactNode } from 'react'; import { Table, UnstyledButton } from '@mantine/core'; -import { - IconSortAscending, - IconSortDescending, - IconArrowsSort, -} from '@tabler/icons-react'; +import { IconArrowsSort, IconSortAscending, IconSortDescending } from '@tabler/icons-react'; import { ColumnDefTemplate, HeaderContext, HeaderGroup } from '@tanstack/react-table'; import classes from './thead.module.css'; type CellData = { - [key: string]: string | Function | boolean | Record; + [key: string]: string | boolean | Record; }; interface TheadProps { - isSortable: boolean, + isSortable: boolean; headerGroups: HeaderGroup[]; flexRender: ( template: ColumnDefTemplate> | undefined, - context: HeaderContext + context: HeaderContext, ) => ReactNode; } @@ -44,17 +40,15 @@ const Thead: FC = ({ isSortable, headerGroups, flexRender }) => ( fz={14} onClick={header.column.getToggleSortingHandler()} > - { - flexRender( - header.column.columnDef.header, - header.getContext(), - ) - } - {isSortable && header.id !== 'select' && ({ - false: , - asc: , - desc: , - }[String(header.column.getIsSorted())] ?? null)} + {flexRender(header.column.columnDef.header, header.getContext())} + {isSortable && + header.id !== 'select' && + ({ + false: , + asc: , + desc: , + }[String(header.column.getIsSorted())] ?? + null)} )} diff --git a/template/apps/web/src/pages/404/index.page.tsx b/template/apps/web/src/pages/404/index.page.tsx index 4fb1e91d..8b1f3acf 100644 --- a/template/apps/web/src/pages/404/index.page.tsx +++ b/template/apps/web/src/pages/404/index.page.tsx @@ -1,7 +1,7 @@ -import { NextPage } from 'next'; +import React, { NextPage } from 'next'; import Head from 'next/head'; import router from 'next/router'; -import { Stack, Title, Text, Button } from '@mantine/core'; +import { Button, Stack, Text, Title } from '@mantine/core'; import { RoutePath } from 'routes'; @@ -11,22 +11,14 @@ const NotFound: NextPage = () => ( Page not found - + Oops! The page is not found. - The page you are looking for may have been removed, - or the link you followed may be broken. + The page you are looking for may have been removed, or the link you followed may be broken. - + ); diff --git a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/MenuToggle/index.tsx b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/MenuToggle/index.tsx index 89c5a494..05520f4b 100644 --- a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/MenuToggle/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/MenuToggle/index.tsx @@ -1,4 +1,4 @@ -import { forwardRef } from 'react'; +import React, { forwardRef } from 'react'; import { Avatar, UnstyledButton, useMantineTheme } from '@mantine/core'; import { accountApi } from 'resources/account'; @@ -20,4 +20,6 @@ const MenuToggle = forwardRef((props, ref) => { ); }); +MenuToggle.displayName = 'MenuToggle'; + export default MenuToggle; diff --git a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/ShadowLoginBanner/index.tsx b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/ShadowLoginBanner/index.tsx index 0023b3b8..6a78a39d 100644 --- a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/ShadowLoginBanner/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/ShadowLoginBanner/index.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import React, { FC } from 'react'; import { Center, Text } from '@mantine/core'; interface ShadowLoginBannerProps { @@ -8,9 +8,10 @@ interface ShadowLoginBannerProps { const ShadowLoginBanner: FC = ({ email }) => (
- You currently under the shadow login as - {' '} - {email} + You currently under the shadow login as{' '} + + {email} +
); diff --git a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/UserMenu/index.tsx b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/UserMenu/index.tsx index 384fe207..cc234101 100644 --- a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/UserMenu/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/components/UserMenu/index.tsx @@ -1,7 +1,7 @@ -import { FC } from 'react'; +import React, { FC } from 'react'; import Link from 'next/link'; import { Menu } from '@mantine/core'; -import { IconUserCircle, IconLogout } from '@tabler/icons-react'; +import { IconLogout, IconUserCircle } from '@tabler/icons-react'; import { accountApi } from 'resources/account'; @@ -19,18 +19,11 @@ const UserMenu: FC = () => { - } - > + }> Profile settings - signOut()} - leftSection={} - > + signOut()} leftSection={}> Log out diff --git a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/index.tsx b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/index.tsx index d9312efc..0c8110b5 100644 --- a/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/MainLayout/Header/index.tsx @@ -1,15 +1,15 @@ -import { memo, FC } from 'react'; -import { Anchor, AppShell, Group } from '@mantine/core'; +import React, { FC, memo } from 'react'; import Link from 'next/link'; +import { Anchor, AppShell, Group } from '@mantine/core'; import { accountApi } from 'resources/account'; -import { RoutePath } from 'routes'; - import { LogoImage } from 'public/images'; -import UserMenu from './components/UserMenu'; +import { RoutePath } from 'routes'; + import ShadowLoginBanner from './components/ShadowLoginBanner'; +import UserMenu from './components/UserMenu'; const Header: FC = () => { const { data: account } = accountApi.useGet(); @@ -21,10 +21,7 @@ const Header: FC = () => { {account.isShadow && } - + diff --git a/template/apps/web/src/pages/_app/PageConfig/MainLayout/index.tsx b/template/apps/web/src/pages/_app/PageConfig/MainLayout/index.tsx index 4d36b574..8f62839d 100644 --- a/template/apps/web/src/pages/_app/PageConfig/MainLayout/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/MainLayout/index.tsx @@ -1,4 +1,4 @@ -import { FC, ReactElement } from 'react'; +import React, { FC, ReactElement } from 'react'; import { AppShell, Stack } from '@mantine/core'; import { accountApi } from 'resources/account'; diff --git a/template/apps/web/src/pages/_app/PageConfig/UnauthorizedLayout/index.tsx b/template/apps/web/src/pages/_app/PageConfig/UnauthorizedLayout/index.tsx index 7d5050ea..010e7a61 100644 --- a/template/apps/web/src/pages/_app/PageConfig/UnauthorizedLayout/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/UnauthorizedLayout/index.tsx @@ -1,21 +1,13 @@ -import { FC, ReactElement } from 'react'; -import { SimpleGrid, Image, Center } from '@mantine/core'; +import React, { FC, ReactElement } from 'react'; +import { Center, Image, SimpleGrid } from '@mantine/core'; interface UnauthorizedLayoutProps { children: ReactElement; } const UnauthorizedLayout: FC = ({ children }) => ( - - App Info + + App Info
{children} diff --git a/template/apps/web/src/pages/_app/PageConfig/index.tsx b/template/apps/web/src/pages/_app/PageConfig/index.tsx index ffc67f43..10b6eb94 100644 --- a/template/apps/web/src/pages/_app/PageConfig/index.tsx +++ b/template/apps/web/src/pages/_app/PageConfig/index.tsx @@ -1,16 +1,16 @@ -import { FC, Fragment, ReactElement } from 'react'; +import React, { FC, Fragment, ReactElement } from 'react'; import { useRouter } from 'next/router'; import { accountApi } from 'resources/account'; import { analyticsService } from 'services'; -import { routesConfiguration, ScopeType, LayoutType, RoutePath } from 'routes'; +import { LayoutType, RoutePath, routesConfiguration, ScopeType } from 'routes'; import config from 'config'; import MainLayout from './MainLayout'; -import UnauthorizedLayout from './UnauthorizedLayout'; import PrivateScope from './PrivateScope'; +import UnauthorizedLayout from './UnauthorizedLayout'; import 'resources/user/user.handlers'; @@ -32,7 +32,7 @@ const PageConfig: FC = ({ children }) => { const { route, push } = useRouter(); const { data: account, isLoading: isAccountLoading } = accountApi.useGet({ onSettled: () => { - if (!config.MIXPANEL_API_KEY) return null; + if (!config.MIXPANEL_API_KEY) return; analyticsService.init(); @@ -58,9 +58,7 @@ const PageConfig: FC = ({ children }) => { return ( - - {children} - + {children} ); }; diff --git a/template/apps/web/src/pages/_app/index.tsx b/template/apps/web/src/pages/_app/index.tsx index 1833d209..388c7141 100644 --- a/template/apps/web/src/pages/_app/index.tsx +++ b/template/apps/web/src/pages/_app/index.tsx @@ -1,21 +1,22 @@ -import { FC } from 'react'; -import Head from 'next/head'; +import React, { FC } from 'react'; import { AppProps } from 'next/app'; +import Head from 'next/head'; import { MantineProvider } from '@mantine/core'; -import { Notifications } from '@mantine/notifications'; import { ModalsProvider } from '@mantine/modals'; +import { Notifications } from '@mantine/notifications'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import '@mantine/core/styles.css'; -import '@mantine/dates/styles.css'; -import '@mantine/notifications/styles.css'; +import theme from 'theme'; import queryClient from 'query-client'; -import theme from 'theme'; import PageConfig from './PageConfig'; +import '@mantine/core/styles.css'; +import '@mantine/dates/styles.css'; +import '@mantine/notifications/styles.css'; + const App: FC = ({ Component, pageProps }) => ( <> diff --git a/template/apps/web/src/pages/_document/index.tsx b/template/apps/web/src/pages/_document/index.tsx index bd62b4b8..b908c8af 100644 --- a/template/apps/web/src/pages/_document/index.tsx +++ b/template/apps/web/src/pages/_document/index.tsx @@ -1,4 +1,5 @@ -import { Html, Head, Main, NextScript } from 'next/document'; +import React from 'react'; +import { Head, Html, Main, NextScript } from 'next/document'; import { ColorSchemeScript } from '@mantine/core'; const Document = () => ( diff --git a/template/apps/web/src/pages/expire-token/index.page.tsx b/template/apps/web/src/pages/expire-token/index.page.tsx index 3ddaa8ef..c718d547 100644 --- a/template/apps/web/src/pages/expire-token/index.page.tsx +++ b/template/apps/web/src/pages/expire-token/index.page.tsx @@ -1,18 +1,19 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; +import { NextPage } from 'next'; import Head from 'next/head'; import { useRouter } from 'next/router'; -import { NextPage } from 'next'; -import { Stack, Title, Text, Button } from '@mantine/core'; +import { Button, Stack, Text, Title } from '@mantine/core'; import { accountApi } from 'resources/account'; import { handleError } from 'utils'; + import { RoutePath } from 'routes'; import { QueryParam } from 'types'; type ForgotPasswordParams = { - email: QueryParam, + email: QueryParam; }; const ForgotPassword: NextPage = () => { @@ -22,15 +23,16 @@ const ForgotPassword: NextPage = () => { const [isSent, setSent] = useState(false); - const { - mutate: resendEmail, - isPending: isResendEmailPending, - } = accountApi.useResendEmail(); + const { mutate: resendEmail, isPending: isResendEmailPending } = accountApi.useResendEmail(); - const onSubmit = () => resendEmail({ email }, { - onSuccess: () => setSent(true), - onError: (e) => handleError(e), - }); + const onSubmit = () => + resendEmail( + { email }, + { + onSuccess: () => setSent(true), + onError: (e) => handleError(e), + }, + ); if (isSent) { return ( @@ -43,9 +45,7 @@ const ForgotPassword: NextPage = () => { Reset link has been sent Reset link sent successfully - + ); @@ -60,17 +60,10 @@ const ForgotPassword: NextPage = () => { Password reset link expired - - Sorry, your password reset link has expired. Click the button below to get a new one. - - - diff --git a/template/apps/web/src/pages/forgot-password/index.page.tsx b/template/apps/web/src/pages/forgot-password/index.page.tsx index 22b51b65..32befc22 100644 --- a/template/apps/web/src/pages/forgot-password/index.page.tsx +++ b/template/apps/web/src/pages/forgot-password/index.page.tsx @@ -1,26 +1,27 @@ -import { z } from 'zod'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter } from 'next/router'; -import Head from 'next/head'; +import React, { useState } from 'react'; import { NextPage } from 'next'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; import { Anchor, Button, Group, Stack, Text, TextInput, Title } from '@mantine/core'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { accountApi } from 'resources/account'; import { handleError } from 'utils'; + import { RoutePath } from 'routes'; import { EMAIL_REGEX } from 'app-constants'; -import Link from 'next/link'; const schema = z.object({ email: z.string().regex(EMAIL_REGEX, 'Email format is incorrect.'), }); type ForgotPasswordParams = { - email: string, + email: string; }; const ForgotPassword: NextPage = () => { @@ -28,10 +29,8 @@ const ForgotPassword: NextPage = () => { const [email, setEmail] = useState(''); - const { - mutate: forgotPassword, - isPending: isForgotPasswordPending, - } = accountApi.useForgotPassword(); + const { mutate: forgotPassword, isPending: isForgotPasswordPending } = + accountApi.useForgotPassword(); const { register, @@ -41,10 +40,11 @@ const ForgotPassword: NextPage = () => { resolver: zodResolver(schema), }); - const onSubmit = (data: ForgotPasswordParams) => forgotPassword(data, { - onSuccess: () => setEmail(data.email), - onError: (e) => handleError(e), - }); + const onSubmit = (data: ForgotPasswordParams) => + forgotPassword(data, { + onSuccess: () => setEmail(data.email), + onError: (e) => handleError(e), + }); if (email) { return ( @@ -56,16 +56,14 @@ const ForgotPassword: NextPage = () => { Reset link has been sent - A link to reset your password has just been sent to - {' '} - {email} - . Please check your email inbox and follow the - directions to reset your password. + A link to reset your password has just been sent to{' '} + + {email} + + . Please check your email inbox and follow the directions to reset your password. - + ); @@ -80,9 +78,7 @@ const ForgotPassword: NextPage = () => { Forgot Password - - Please enter your email and we'll send a link to reset your password. - + Please enter your email and we'll send a link to reset your password.
@@ -94,10 +90,7 @@ const ForgotPassword: NextPage = () => { error={errors.email?.message} /> - @@ -105,10 +98,7 @@ const ForgotPassword: NextPage = () => { Have an account? - + Sign in diff --git a/template/apps/web/src/pages/home/constants.ts b/template/apps/web/src/pages/home/constants.ts index 3388fa63..e855e092 100644 --- a/template/apps/web/src/pages/home/constants.ts +++ b/template/apps/web/src/pages/home/constants.ts @@ -1,5 +1,5 @@ -import { ColumnDef } from '@tanstack/react-table'; import { ComboboxItem } from '@mantine/core'; +import { ColumnDef } from '@tanstack/react-table'; import { User } from 'types'; diff --git a/template/apps/web/src/pages/home/index.module.css b/template/apps/web/src/pages/home/index.module.css index d6a5d4d3..8cc99033 100644 --- a/template/apps/web/src/pages/home/index.module.css +++ b/template/apps/web/src/pages/home/index.module.css @@ -1,5 +1,5 @@ .inputSkeleton { - flex-grow: 0.25 + flex-grow: 0.25; } .datePickerSkeleton { diff --git a/template/apps/web/src/pages/home/index.tsx b/template/apps/web/src/pages/home/index.tsx index 957b14fa..f8471a22 100644 --- a/template/apps/web/src/pages/home/index.tsx +++ b/template/apps/web/src/pages/home/index.tsx @@ -1,21 +1,11 @@ -import { useCallback, useLayoutEffect, useState } from 'react'; -import Head from 'next/head'; +import React, { useCallback, useLayoutEffect, useState } from 'react'; import { NextPage } from 'next'; -import { - Select, - TextInput, - Group, - Title, - Stack, - Skeleton, - Text, - Container, - ActionIcon, -} from '@mantine/core'; +import Head from 'next/head'; +import { ActionIcon, Container, Group, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; +import { DatePickerInput, DatesRangeValue, DateValue } from '@mantine/dates'; import { useDebouncedValue, useInputState } from '@mantine/hooks'; -import { IconSearch, IconX, IconSelector } from '@tabler/icons-react'; +import { IconSearch, IconSelector, IconX } from '@tabler/icons-react'; import { RowSelectionState, SortingState } from '@tanstack/react-table'; -import { DatePickerInput, DatesRangeValue, DateValue } from '@mantine/dates'; import { userApi } from 'resources/user'; @@ -23,7 +13,7 @@ import { Table } from 'components'; import { ListParams, SortOrder } from 'types'; -import { PER_PAGE, columns, selectOptions } from './constants'; +import { columns, PER_PAGE, selectOptions } from './constants'; import classes from './index.module.css'; @@ -107,23 +97,17 @@ const Home: NextPage = () => { onChange={setSearch} placeholder="Search by name or email" leftSection={} - rightSection={search && ( - setSearch('')} - > - - - )} + rightSection={ + search && ( + setSearch('')}> + + + ) + } /> - +