Browse Source

Merge pull request #5444 from nocodb/feat/backend-refactoring-nest

Moving backend to nestjs
pull/5508/head
Raju Udava 1 year ago committed by GitHub
parent
commit
fa43cb9e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .github/workflows/ci-cd.yml
  2. 14
      .github/workflows/playwright-test-workflow.yml
  3. 78
      packages/nocodb-nest/.eslintrc.js
  4. 38
      packages/nocodb-nest/.gitignore
  5. 4
      packages/nocodb-nest/.prettierrc
  6. 72
      packages/nocodb-nest/Dockerfile
  7. 73
      packages/nocodb-nest/README.md
  8. 32
      packages/nocodb-nest/docker/start-litestream.sh
  9. 14
      packages/nocodb-nest/docker/start-local.sh
  10. 10
      packages/nocodb-nest/docker/start.sh
  11. 46
      packages/nocodb-nest/docker/webpack.config.js
  12. 8
      packages/nocodb-nest/nest-cli.json
  13. 32179
      packages/nocodb-nest/package-lock.json
  14. 196
      packages/nocodb-nest/package.json
  15. 288
      packages/nocodb-nest/public/css/fonts.montserrat.css
  16. 336
      packages/nocodb-nest/public/css/fonts.roboto.css
  17. 3
      packages/nocodb-nest/public/css/materialdesignicons.5.x.min.css
  18. 1
      packages/nocodb-nest/public/css/swagger-ui-bundle.4.5.2.min.css
  19. 9
      packages/nocodb-nest/public/css/vuetify.2.x.min.css
  20. BIN
      packages/nocodb-nest/public/favicon.ico
  21. BIN
      packages/nocodb-nest/public/icon.png
  22. 3
      packages/nocodb-nest/public/js/axios.0.19.2.min.js
  23. 1806
      packages/nocodb-nest/public/js/redoc.standalone.min.js
  24. 1
      packages/nocodb-nest/public/js/swagger-ui-bundle.4.5.2.min.js
  25. 6
      packages/nocodb-nest/public/js/vue.2.6.14.min.js
  26. 16159
      packages/nocodb-nest/public/js/vue.global.js
  27. 6
      packages/nocodb-nest/public/js/vuetify.2.x.min.js
  28. 117
      packages/nocodb-nest/src/Noco.ts
  29. 85
      packages/nocodb-nest/src/app.module.ts
  30. 31
      packages/nocodb-nest/src/cache/CacheMgr.ts
  31. 112
      packages/nocodb-nest/src/cache/NocoCache.ts
  32. 277
      packages/nocodb-nest/src/cache/RedisCacheMgr.ts
  33. 277
      packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts
  34. 19
      packages/nocodb-nest/src/connection/connection.spec.ts
  35. 37
      packages/nocodb-nest/src/connection/connection.ts
  36. 4
      packages/nocodb-nest/src/constants/index.ts
  37. 21
      packages/nocodb-nest/src/controllers/api-docs/api-docs.controller.spec.ts
  38. 43
      packages/nocodb-nest/src/controllers/api-docs/api-docs.controller.ts
  39. 93
      packages/nocodb-nest/src/controllers/api-docs/template/redocHtml.ts
  40. 87
      packages/nocodb-nest/src/controllers/api-docs/template/swaggerHtml.ts
  41. 21
      packages/nocodb-nest/src/controllers/api-tokens.controller.spec.ts
  42. 52
      packages/nocodb-nest/src/controllers/api-tokens.controller.ts
  43. 21
      packages/nocodb-nest/src/controllers/attachments.controller.spec.ts
  44. 100
      packages/nocodb-nest/src/controllers/attachments.controller.ts
  45. 21
      packages/nocodb-nest/src/controllers/audits.controller.spec.ts
  46. 96
      packages/nocodb-nest/src/controllers/audits.controller.ts
  47. 21
      packages/nocodb-nest/src/controllers/auth.controller.spec.ts
  48. 48
      packages/nocodb-nest/src/controllers/auth.controller.ts
  49. 21
      packages/nocodb-nest/src/controllers/bases.controller.spec.ts
  50. 89
      packages/nocodb-nest/src/controllers/bases.controller.ts
  51. 19
      packages/nocodb-nest/src/controllers/bulk-data-alias.controller.spec.ts
  52. 112
      packages/nocodb-nest/src/controllers/bulk-data-alias.controller.ts
  53. 21
      packages/nocodb-nest/src/controllers/caches.controller.spec.ts
  54. 25
      packages/nocodb-nest/src/controllers/caches.controller.ts
  55. 21
      packages/nocodb-nest/src/controllers/columns.controller.spec.ts
  56. 74
      packages/nocodb-nest/src/controllers/columns.controller.ts
  57. 21
      packages/nocodb-nest/src/controllers/data-alias-export.controller.spec.ts
  58. 68
      packages/nocodb-nest/src/controllers/data-alias-export.controller.ts
  59. 21
      packages/nocodb-nest/src/controllers/data-alias-nested.controller.spec.ts
  60. 174
      packages/nocodb-nest/src/controllers/data-alias-nested.controller.ts
  61. 19
      packages/nocodb-nest/src/controllers/data-alias-nested.service.spec.ts
  62. 21
      packages/nocodb-nest/src/controllers/data-alias.controller.spec.ts
  63. 250
      packages/nocodb-nest/src/controllers/data-alias.controller.ts
  64. 21
      packages/nocodb-nest/src/controllers/datas.controller.spec.ts
  65. 215
      packages/nocodb-nest/src/controllers/datas.controller.ts
  66. 21
      packages/nocodb-nest/src/controllers/filters.controller.spec.ts
  67. 113
      packages/nocodb-nest/src/controllers/filters.controller.ts
  68. 21
      packages/nocodb-nest/src/controllers/form-columns.controller.spec.ts
  69. 28
      packages/nocodb-nest/src/controllers/form-columns.controller.ts
  70. 21
      packages/nocodb-nest/src/controllers/forms.controller.spec.ts
  71. 54
      packages/nocodb-nest/src/controllers/forms.controller.ts
  72. 21
      packages/nocodb-nest/src/controllers/galleries.controller.spec.ts
  73. 58
      packages/nocodb-nest/src/controllers/galleries.controller.ts
  74. 21
      packages/nocodb-nest/src/controllers/grid-columns.controller.spec.ts
  75. 34
      packages/nocodb-nest/src/controllers/grid-columns.controller.ts
  76. 21
      packages/nocodb-nest/src/controllers/grids.controller.spec.ts
  77. 47
      packages/nocodb-nest/src/controllers/grids.controller.ts
  78. 21
      packages/nocodb-nest/src/controllers/hooks.controller.spec.ts
  79. 114
      packages/nocodb-nest/src/controllers/hooks.controller.ts
  80. 222
      packages/nocodb-nest/src/controllers/imports/helpers/EntityMap.ts
  81. 6
      packages/nocodb-nest/src/controllers/imports/helpers/NocoSyncDestAdapter.ts
  82. 7
      packages/nocodb-nest/src/controllers/imports/helpers/NocoSyncSourceAdapter.ts
  83. 242
      packages/nocodb-nest/src/controllers/imports/helpers/fetchAT.ts
  84. 2480
      packages/nocodb-nest/src/controllers/imports/helpers/job.ts
  85. 361
      packages/nocodb-nest/src/controllers/imports/helpers/readAndProcessData.ts
  86. 31
      packages/nocodb-nest/src/controllers/imports/helpers/syncMap.ts
  87. 21
      packages/nocodb-nest/src/controllers/imports/import.controller.spec.ts
  88. 148
      packages/nocodb-nest/src/controllers/imports/import.controller.ts
  89. 21
      packages/nocodb-nest/src/controllers/kanbans.controller.spec.ts
  90. 56
      packages/nocodb-nest/src/controllers/kanbans.controller.ts
  91. 21
      packages/nocodb-nest/src/controllers/maps.controller.spec.ts
  92. 56
      packages/nocodb-nest/src/controllers/maps.controller.ts
  93. 21
      packages/nocodb-nest/src/controllers/meta-diffs.controller.spec.ts
  94. 61
      packages/nocodb-nest/src/controllers/meta-diffs.controller.ts
  95. 23
      packages/nocodb-nest/src/controllers/model-visibilities.controller.spec.ts
  96. 52
      packages/nocodb-nest/src/controllers/model-visibilities.controller.ts
  97. 19
      packages/nocodb-nest/src/controllers/old-datas/old-datas.controller.spec.ts
  98. 138
      packages/nocodb-nest/src/controllers/old-datas/old-datas.controller.ts
  99. 19
      packages/nocodb-nest/src/controllers/old-datas/old-datas.service.spec.ts
  100. 142
      packages/nocodb-nest/src/controllers/old-datas/old-datas.service.ts
  101. Some files were not shown because too many files have changed in this diff Show More

10
.github/workflows/ci-cd.yml

@ -9,6 +9,7 @@ on:
paths:
- "packages/nc-gui/**"
- "packages/nocodb/**"
- "packages/nocodb-nest/**"
- ".github/workflows/ci-cd.yml"
- "tests/playwright/**"
pull_request:
@ -17,6 +18,7 @@ on:
paths:
- "packages/nc-gui/**"
- "packages/nocodb/**"
- "packages/nocodb-nest/**"
- ".github/workflows/ci-cd.yml"
- "tests/playwright/**"
@ -58,10 +60,10 @@ jobs:
working-directory: ./packages/nocodb-sdk
run: npm run build:main
- name: Install dependencies
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: npm install
- name: run unit tests
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: npm run test:unit
unit-tests-pg:
runs-on: ubuntu-20.04
@ -99,10 +101,10 @@ jobs:
working-directory: ./packages/nocodb-sdk
run: npm run build:main
- name: Install dependencies
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: npm install
- name: run unit tests
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: npm run test:unit:pg
playwright-mysql-1:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}

14
.github/workflows/playwright-test-workflow.yml

@ -69,7 +69,7 @@ jobs:
working-directory: ./packages/nc-gui
run: npm run ci:run
- name: Run backend
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: |
npm install
npm run watch:run:playwright > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
@ -109,14 +109,14 @@ jobs:
# Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
- name: Run quick server and tests (pg)
if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
working-directory: ./packages/nocodb
working-directory: ./packages/nocodb-nest
run: |
kill -9 $(lsof -t -i:8080)
npm run watch:run:playwright:pg:cyquick &
- name: Run quick server and tests (sqlite)
if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
working-directory: ./packages/nocodb
run: |
working-directory: ./packages/nocodb-nest
run: |
kill -9 $(lsof -t -i:8080)
npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
- name: Wait for backend & run quick tests
@ -132,7 +132,7 @@ jobs:
if: ${{ inputs.db == 'sqlite' }}
with:
name: quick-backend-log-${{ inputs.shard }}
path: ./packages/nocodb/quick_${{ inputs.shard }}_test_backend.log
path: ./packages/nocodb-nest/quick_${{ inputs.shard }}_test_backend.log
retention-days: 2
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}
@ -157,5 +157,5 @@ jobs:
if: always()
with:
name: backend-logs-${{ inputs.db }}-${{ inputs.shard }}
path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log
retention-days: 2
path: ./packages/nocodb-nest/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log
retention-days: 2

78
packages/nocodb-nest/.eslintrc.js

@ -0,0 +1,78 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['import', 'eslint-comments', 'functional'],
extends: [
'eslint:recommended',
'plugin:eslint-comments/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/typescript',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
es6: true,
},
ignorePatterns: [
'node_modules',
'build',
'coverage',
'dist',
'nc',
'.eslintrc.js',
],
globals: {
BigInt: true,
console: true,
WebAssembly: true,
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'eslint-comments/disable-enable-pair': [
'error',
{
allowWholeFile: true,
},
],
'eslint-comments/no-unused-disable': 'error',
'sort-imports': [
'error',
{
ignoreDeclarationSort: true,
ignoreCase: true,
},
],
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
},
],
'@typescript-eslint/no-this-alias': 'off',
// todo: enable
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-useless-catch': 'off',
'no-empty': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/consistent-type-imports': 'warn',
},
};

38
packages/nocodb-nest/.gitignore vendored

@ -0,0 +1,38 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/noco.db
/docker/main.js

4
packages/nocodb-nest/.prettierrc

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

72
packages/nocodb-nest/Dockerfile

@ -0,0 +1,72 @@
###########
# Litestream Builder
###########
FROM golang:alpine3.14 as lt-builder
WORKDIR /usr/src/
RUN apk add --no-cache git make musl-dev gcc
# build litestream
RUN git clone https://github.com/benbjohnson/litestream.git litestream
RUN cd litestream ; go install ./cmd/litestream
RUN cp $GOPATH/bin/litestream /usr/src/lt
###########
# Builder
###########
FROM node:16.17.0-alpine3.15 as builder
WORKDIR /usr/src/app
# install node-gyp dependencies
RUN apk add --no-cache python3 make g++
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm ci on every code change.
COPY ./package*.json ./
COPY ./docker/main.js ./docker/main.js
#COPY ./docker/start.sh /usr/src/appEntry/start.sh
COPY ./docker/start-litestream.sh /usr/src/appEntry/start.sh
COPY ./src/lib/public/css/*.css ./docker/public/css/
COPY ./src/lib/public/js/*.js ./docker/public/js/
COPY ./src/lib/public/favicon.ico ./docker/public/
# install production dependencies,
# reduce node_module size with modclean & removing sqlite deps,
# package built code into app.tar.gz & add execute permission to start.sh
RUN npm ci --omit=dev --quiet \
&& npx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**,@azure/msal-node/dist/**" --run \
&& rm -rf ./node_modules/sqlite3/deps \
&& tar -czf ../appEntry/app.tar.gz ./* \
&& chmod +x /usr/src/appEntry/start.sh
##########
# Runner
##########
FROM alpine:3.15
WORKDIR /usr/src/app
ENV NC_DOCKER 0.6
ENV NODE_ENV production
ENV PORT 8080
ENV NC_TOOL_DIR=/usr/app/data/
RUN apk --update --no-cache add \
nodejs \
tar \
dumb-init
# Copy litestream binary build
COPY --from=lt-builder /usr/src/lt /usr/src/appEntry/litestream
# Copy packaged production code & main entry file
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# Start Nocodb
CMD ["/usr/src/appEntry/start.sh"]

73
packages/nocodb-nest/README.md

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

32
packages/nocodb-nest/docker/start-litestream.sh

@ -0,0 +1,32 @@
#!/bin/sh
FILE="/usr/src/app/package.json"
#sleep 5
if [ ! -z "${NC_TOOL_DIR}" ]; then
mkdir -p $NC_TOOL_DIR
fi
if [[ ! -z "${AWS_ACCESS_KEY_ID}" && ! -z "${AWS_SECRET_ACCESS_KEY}" && ! -z "${AWS_BUCKET}" && ! -z "${AWS_BUCKET_PATH}" ]]; then
if [ -f "${NC_TOOL_DIR}noco.db" ]
then
rm "${NC_TOOL_DIR}noco.db"
rm "${NC_TOOL_DIR}noco.db-shm"
rm "${NC_TOOL_DIR}noco.db-wal"
fi
/usr/src/appEntry/litestream restore -o "${NC_TOOL_DIR}noco.db" s3://$AWS_BUCKET/$AWS_BUCKET_PATH;
if [ ! -f "${NC_TOOL_DIR}noco.db" ]
then
touch "${NC_TOOL_DIR}noco.db"
fi
/usr/src/appEntry/litestream replicate "${NC_TOOL_DIR}noco.db" s3://$AWS_BUCKET/$AWS_BUCKET_PATH &
fi
if [ ! -f "$FILE" ]
then
tar -xzf /usr/src/appEntry/app.tar.gz -C /usr/src/app/
fi
node docker/main.js

14
packages/nocodb-nest/docker/start-local.sh

@ -0,0 +1,14 @@
#!/bin/sh
FILE="/usr/src/app/package.json"
if [ ! -z "${NC_TOOL_DIR}" ]; then
mkdir -p $NC_TOOL_DIR
fi
if [ ! -f "$FILE" ]
then
tar -xzf /usr/src/appEntry/app.tar.gz -C /usr/src/app/
fi
node docker/index.js

10
packages/nocodb-nest/docker/start.sh

@ -0,0 +1,10 @@
#!/bin/sh
FILE="/usr/src/app/package.json"
if [ ! -f "$FILE" ]
then
tar -xzf /usr/src/appEntry/app.tar.gz -C /usr/src/app/
fi
node docker/main.js

46
packages/nocodb-nest/docker/webpack.config.js

@ -0,0 +1,46 @@
const nodeExternals = require('webpack-node-externals');
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: './src/run/dockerEntry.ts',
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
},
output: {
path: require('path').resolve("./docker"),
filename: "main.js",
library: 'libs',
libraryTarget: 'umd',
globalObject: "typeof self !== 'undefined' ? self : this"
},
optimization: {
minimize: true, //Update this to true or false
minimizer: [new TerserPlugin()],
nodeEnv:false
},
externals: [nodeExternals()],
plugins: [
new webpack.EnvironmentPlugin([
'EE'
]),
],
target: 'node',
node: {
__dirname: false,
},
};

8
packages/nocodb-nest/nest-cli.json

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

32179
packages/nocodb-nest/package-lock.json generated

File diff suppressed because it is too large Load Diff

196
packages/nocodb-nest/package.json

@ -0,0 +1,196 @@
{
"name": "nocodb-nest",
"version": "0.0.1",
"description": "NocoDB Backend (Nest)",
"main": "dist/bundle.js",
"author": {
"name": "NocoDB Inc",
"url": "https://nocodb.com/"
},
"homepage": "https://github.com/nocodb/nocodb",
"repository": {
"type": "git",
"url": "https://github.com/nocodb/nocodb.git"
},
"bugs": {
"url": "https://github.com/nocodb/nocodb/issues"
},
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"build": "nest build",
"build:obfuscate": "EE=true webpack --config webpack.config.js",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"watch:run:playwright": "rm -f ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright:quick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../tests/playwright/fixtures/noco_0_91_7.db ./test_noco.db; cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"test:unit": "cross-env TS_NODE_PROJECT=./tests/unit/tsconfig.json mocha -r ts-node/register tests/unit/index.test.ts --recursive --timeout 300000 --exit --delay",
"test:unit:pg": "cp tests/unit/.pg.env tests/unit/.env; cross-env TS_NODE_PROJECT=./tests/unit/tsconfig.json mocha -r ts-node/register tests/unit/index.test.ts --recursive --timeout 300000 --exit --delay",
"docker:build": "EE=\"true-xc-test\" webpack --config docker/webpack.config.js"
},
"dependencies": {
"@google-cloud/storage": "^5.7.2",
"@graphql-tools/merge": "^6.0.12",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/jwt": "^10.0.3",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.4.0",
"@nestjs/serve-static": "^3.0.1",
"@sentry/node": "^6.3.5",
"@types/chai": "^4.2.12",
"airtable": "^0.11.3",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.0.2",
"auto-bind": "^4.0.0",
"aws-sdk": "^2.829.0",
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"boxen": "^5.0.0",
"bullmq": "^1.81.1",
"clear": "^0.1.0",
"colors": "^1.4.0",
"compare-versions": "^6.0.0-rc.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cron": "^1.8.2",
"crypto-js": "^4.0.0",
"dataloader": "^2.0.0",
"dayjs": "^1.8.34",
"debug": "^4.2.0",
"dotenv": "^8.2.0",
"ejs": "^3.1.3",
"emittery": "^0.7.1",
"express": "^4.17.1",
"express-graphql": "^0.11.0",
"extract-zip": "^2.0.1",
"fast-levenshtein": "^2.0.6",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6",
"import-fresh": "^3.2.1",
"inflection": "^1.12.0",
"ioredis": "^4.28.5",
"ioredis-mock": "^7.1.0",
"is-docker": "^2.2.1",
"isomorphic-dompurify": "^0.19.0",
"jsep": "^1.3.6",
"jsonfile": "^6.1.0",
"jsonwebtoken": "^9.0.0",
"knex": "2.2.0",
"lodash": "^4.17.19",
"lru-cache": "^6.0.0",
"mailersend": "^1.1.0",
"minio": "^7.0.18",
"mkdirp": "^2.1.3",
"morgan": "^1.10.0",
"mssql": "^6.2.0",
"multer": "^1.4.4",
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.106.0",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
"nodemailer": "^6.4.10",
"object-hash": "^3.0.0",
"os-locale": "^6.0.2",
"papaparse": "^5.3.1",
"parse-database-url": "^0.3.0",
"passport": "^0.4.1",
"passport-auth-token": "^1.0.1",
"passport-custom": "^1.1.1",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.10.0",
"reflect-metadata": "^0.1.13",
"request": "^2.88.2",
"request-ip": "^2.1.3",
"rmdir": "^1.2.0",
"rxjs": "^7.2.0",
"slash": "^3.0.0",
"socket.io": "^4.4.1",
"sqlite3": "^5.1.6",
"tedious": "^15.0.0",
"tinycolor2": "^1.4.2",
"twilio": "^3.55.1",
"unique-names-generator": "^4.3.1",
"uuid": "^9.0.0",
"validator": "^13.1.1",
"xc-core-ts": "^0.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@nestjsplus/dyn-schematics": "^1.0.12",
"@types/express": "^4.17.13",
"@types/jest": "^29.5.0",
"@types/mocha": "^10.0.1",
"@types/multer": "^1.4.7",
"@types/node": "18.15.11",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"chai": "^4.2.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.5.0",
"mocha": "^10.1.0",
"nodemon": "^2.0.22",
"prettier": "^2.7.1",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typescript": "^4.7.4",
"webpack-cli": "^5.0.1"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

288
packages/nocodb-nest/public/css/fonts.montserrat.css

@ -0,0 +1,288 @@
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/montserrat/v25/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

336
packages/nocodb-nest/public/css/fonts.roboto.css

@ -0,0 +1,336 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCRc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfABc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCBc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfBxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCxc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfChc4AMP6lbBP.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfBBc4AMP6lQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

3
packages/nocodb-nest/public/css/materialdesignicons.5.x.min.css vendored

File diff suppressed because one or more lines are too long

1
packages/nocodb-nest/public/css/swagger-ui-bundle.4.5.2.min.css vendored

File diff suppressed because one or more lines are too long

9
packages/nocodb-nest/public/css/vuetify.2.x.min.css vendored

File diff suppressed because one or more lines are too long

BIN
packages/nocodb-nest/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
packages/nocodb-nest/public/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

3
packages/nocodb-nest/public/js/axios.0.19.2.min.js vendored

File diff suppressed because one or more lines are too long

1806
packages/nocodb-nest/public/js/redoc.standalone.min.js vendored

File diff suppressed because one or more lines are too long

1
packages/nocodb-nest/public/js/swagger-ui-bundle.4.5.2.min.js vendored

File diff suppressed because one or more lines are too long

6
packages/nocodb-nest/public/js/vue.2.6.14.min.js vendored

File diff suppressed because one or more lines are too long

16159
packages/nocodb-nest/public/js/vue.global.js

File diff suppressed because it is too large Load Diff

6
packages/nocodb-nest/public/js/vuetify.2.x.min.js vendored

File diff suppressed because one or more lines are too long

117
packages/nocodb-nest/src/Noco.ts

@ -0,0 +1,117 @@
// import * as Sentry from '@sentry/node';
import { NestFactory } from '@nestjs/core';
import clear from 'clear';
import * as express from 'express';
import NcToolGui from 'nc-lib-gui';
import { AppModule } from './app.module';
import { NC_LICENSE_KEY } from './constants';
import Store from './models/Store';
import type { Express } from 'express';
import type * as http from 'http';
export default class Noco {
private static _this: Noco;
private static ee: boolean;
public static readonly env: string = '_noco';
private static _httpServer: http.Server;
private static _server: Express;
public static get dashboardUrl(): string {
let siteUrl = `http://localhost:${process.env.PORT || 8080}`;
// if (Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl) {
// siteUrl = Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl;
// }
if (Noco._this?.config?.envs?.['_noco']?.publicUrl) {
siteUrl = Noco._this?.config?.envs?.['_noco']?.publicUrl;
}
return `${siteUrl}${Noco._this?.config?.dashboardPath}`;
}
public static config: any;
public readonly router: express.Router;
public readonly projectRouter: express.Router;
public static _ncMeta: any;
public readonly metaMgr: any;
public readonly metaMgrv2: any;
public env: string;
private ncToolApi;
private config: any;
private requestContext: any;
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {
process.env.NC_CONNECT_TO_EXTERNAL_DB_DISABLED = 'true';
}
this.router = express.Router();
this.projectRouter = express.Router();
clear();
/******************* prints : end *******************/
}
public getConfig(): any {
return this.config;
}
public getToolDir(): string {
return this.getConfig()?.toolDir;
}
public addToContext(context: any) {
this.requestContext = context;
}
public static get ncMeta(): any {
return this._ncMeta;
}
public get ncMeta(): any {
return Noco._ncMeta;
}
public static getConfig(): any {
return Noco.config;
}
public static isEE(): boolean {
return Noco.ee;
}
public static async loadEEState(): Promise<boolean> {
try {
return (Noco.ee = !!(await Store.get(NC_LICENSE_KEY))?.value);
} catch {}
return (Noco.ee = false);
}
static async init(param: any, httpServer: http.Server, server: Express) {
this._httpServer = httpServer;
this._server = server;
const nestApp = await NestFactory.create(AppModule);
nestApp.use(
express.json({ limit: process.env.NC_REQUEST_BODY_SIZE || '50mb' }),
);
await nestApp.init();
const dashboardPath = process.env.NC_DASHBOARD_URL || '/dashboard';
server.use(NcToolGui.expressMiddleware(dashboardPath));
server.get('/', (_req, res) => res.redirect(dashboardPath));
return nestApp.getHttpAdapter().getInstance();
}
public static get httpServer(): http.Server {
return Noco._httpServer;
}
public static get server(): Express {
return Noco._server;
}
}

85
packages/nocodb-nest/src/app.module.ts

@ -0,0 +1,85 @@
import { Module, RequestMethod } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { Connection } from './connection/connection';
import { GlobalExceptionFilter } from './filters/global-exception/global-exception.filter';
import NcPluginMgrv2 from './helpers/NcPluginMgrv2';
import { GlobalMiddleware } from './middlewares/global/global.middleware';
import { GuiMiddleware } from './middlewares/gui/gui.middleware';
import { PublicMiddleware } from './middlewares/public/public.middleware';
import { DatasModule } from './modules/datas/datas.module';
import { AuthService } from './services/auth.service';
import { UsersModule } from './modules/users/users.module';
import { MetaService } from './meta/meta.service';
import Noco from './Noco';
import { TestModule } from './modules/test/test.module';
import { GlobalModule } from './modules/global/global.module';
import { LocalStrategy } from './strategies/local.strategy';
import { AuthTokenStrategy } from './strategies/authtoken.strategy/authtoken.strategy';
import { BaseViewStrategy } from './strategies/base-view.strategy/base-view.strategy';
import NcUpgrader from './version-upgrader/NcUpgrader';
import { MetasModule } from './modules/metas/metas.module';
import NocoCache from './cache/NocoCache';
import type {
MiddlewareConsumer,
OnApplicationBootstrap,
} from '@nestjs/common';
@Module({
imports: [
GlobalModule,
UsersModule,
...(process.env['PLAYWRIGHT_TEST'] === 'true' ? [TestModule] : []),
MetasModule,
DatasModule,
],
controllers: [],
providers: [
AuthService,
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
LocalStrategy,
AuthTokenStrategy,
BaseViewStrategy,
],
})
export class AppModule implements OnApplicationBootstrap {
constructor(
private readonly connection: Connection,
private readonly metaService: MetaService,
) {}
// Global Middleware
configure(consumer: MiddlewareConsumer) {
consumer
.apply(GuiMiddleware)
.forRoutes({ path: '*', method: RequestMethod.GET })
.apply(PublicMiddleware)
.forRoutes({ path: '*', method: RequestMethod.GET })
.apply(GlobalMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
// app init
async onApplicationBootstrap(): Promise<void> {
process.env.NC_VERSION = '0105004';
await NocoCache.init();
await this.connection.init();
await this.metaService.init();
// todo: remove
// temporary hack
Noco._ncMeta = this.metaService;
Noco.config = this.connection.config;
// init plugin manager
await NcPluginMgrv2.init(Noco.ncMeta);
await Noco.loadEEState();
// run upgrader
await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta });
}
}

31
packages/nocodb-nest/src/cache/CacheMgr.ts vendored

@ -0,0 +1,31 @@
export default abstract class CacheMgr {
public abstract get(key: string, type: string): Promise<any>;
public abstract set(key: string, value: any): Promise<any>;
public abstract del(key: string): Promise<any>;
public abstract getAll(pattern: string): Promise<any[]>;
public abstract delAll(scope: string, pattern: string): Promise<any[]>;
public abstract getList(
scope: string,
list: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}>;
public abstract setList(
scope: string,
subListKeys: string[],
list: any[],
): Promise<boolean>;
public abstract deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean>;
public abstract appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean>;
public abstract destroy(): Promise<boolean>;
public abstract export(): Promise<any>;
}

112
packages/nocodb-nest/src/cache/NocoCache.ts vendored

@ -0,0 +1,112 @@
import { CacheGetType } from '../utils/globals';
import RedisCacheMgr from './RedisCacheMgr';
import RedisMockCacheMgr from './RedisMockCacheMgr';
import type CacheMgr from './CacheMgr';
export default class NocoCache {
private static client: CacheMgr;
private static cacheDisabled: boolean;
private static prefix: string;
public static init() {
this.cacheDisabled = (process.env.NC_DISABLE_CACHE || false) === 'true';
if (this.cacheDisabled) {
return;
}
if (process.env.NC_REDIS_URL) {
this.client = new RedisCacheMgr(process.env.NC_REDIS_URL);
} else {
this.client = new RedisMockCacheMgr();
}
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
this.prefix = `nc:${orgs}`;
}
public static async set(key, value): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.set(`${this.prefix}:${key}`, value);
}
public static async get(key, type): Promise<any> {
if (this.cacheDisabled) {
if (type === CacheGetType.TYPE_ARRAY) return Promise.resolve([]);
else if (type === CacheGetType.TYPE_OBJECT) return Promise.resolve(null);
return Promise.resolve(null);
}
return this.client.get(`${this.prefix}:${key}`, type);
}
public static async getAll(pattern: string): Promise<any[]> {
if (this.cacheDisabled) return Promise.resolve([]);
return this.client.getAll(`${this.prefix}:${pattern}`);
}
public static async del(key): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.del(`${this.prefix}:${key}`);
}
public static async delAll(scope: string, pattern: string): Promise<any[]> {
if (this.cacheDisabled) return Promise.resolve([]);
return this.client.delAll(scope, pattern);
}
public static async getList(
scope: string,
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}> {
if (this.cacheDisabled)
return Promise.resolve({
list: [],
isNoneList: false,
});
return this.client.getList(scope, subKeys);
}
public static async setList(
scope: string,
subListKeys: string[],
list: any[],
): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.setList(scope, subListKeys, list);
}
public static async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.deepDel(scope, key, direction);
}
public static async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.appendToList(
scope,
subListKeys,
`${this.prefix}:${key}`,
);
}
public static async destroy(): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.destroy();
}
public static async export(): Promise<any> {
if (this.cacheDisabled) return Promise.resolve({});
return this.client.export();
}
}

277
packages/nocodb-nest/src/cache/RedisCacheMgr.ts vendored

@ -0,0 +1,277 @@
import debug from 'debug';
import Redis from 'ioredis';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');
export default class RedisCacheMgr extends CacheMgr {
client: any;
prefix: string;
constructor(config: any) {
super();
this.client = new Redis(config);
// flush the existing db with selected key (Default: 0)
this.client.flushdb();
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
this.prefix = `nc:${orgs}`;
}
// avoid circular structure to JSON
getCircularReplacer = () => {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
// @ts-ignore
async del(key: string): Promise<any> {
log(`RedisCacheMgr::del: deleting key ${key}`);
return this.client.del(key);
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
log(`RedisCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
} else if (type === CacheGetType.TYPE_OBJECT) {
const res = await this.client.get(key);
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
if (
o &&
Object.keys(o).length === 0 &&
Object.getPrototypeOf(o) === Object.prototype
) {
log(`RedisCacheMgr::get: object is empty!`);
}
return Promise.resolve(o);
}
} catch (e) {
return Promise.resolve(res);
}
return Promise.resolve(res);
} else if (type === CacheGetType.TYPE_STRING) {
return await this.client.get(key);
}
log(`Invalid CacheGetType: ${type}`);
return Promise.resolve(false);
}
// @ts-ignore
async set(key: string, value: any): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(`RedisCacheMgr::set: setting key ${key} with value ${value}`);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
);
}
return this.client.set(key, value);
} else {
log(`RedisCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async getAll(pattern: string): Promise<any> {
return this.client.hgetall(pattern);
}
// @ts-ignore
async delAll(scope: string, pattern: string): Promise<any[]> {
// Example: nc:<orgs>:model:*:<id>
const keys = await this.client.keys(`${this.prefix}:${scope}:${pattern}`);
log(
`RedisCacheMgr::delAll: deleting all keys with pattern ${this.prefix}:${scope}:${pattern}`,
);
await Promise.all(
keys.map(async (k) => {
await this.deepDel(scope, k, CacheDelDirection.CHILD_TO_PARENT);
}),
);
return Promise.all(
keys.map(async (k) => {
await this.del(k);
}),
);
}
async getList(
scope: string,
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays
subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const key =
subKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subKeys.join(':')}:list`;
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE';
if (isNoneList) {
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: await Promise.all(
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT)),
),
isNoneList,
};
}
async setList(
scope: string,
subListKeys: string[],
list: any[],
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// construct key for List
// e.g. nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) {
// Set NONE here so that it won't hit the DB on each page load
return this.set(listKey, ['NONE']);
}
// fetch existing list
const listOfGetKeys =
(await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
for (const o of list) {
// construct key for Get
// e.g. nc:<orgs>:<scope>:<model_id_1>
let getKey = `${this.prefix}:${scope}:${o.id}`;
// special case - MODEL_ROLE_VISIBILITY
if (scope === CacheScope.MODEL_ROLE_VISIBILITY) {
getKey = `${this.prefix}:${scope}:${o.id}:${o.role}`;
}
// set Get Key
log(`RedisCacheMgr::setList: setting key ${getKey}`);
await this.set(getKey, JSON.stringify(o, this.getCircularReplacer()));
// push Get Key to List
listOfGetKeys.push(getKey);
}
// set List Key
log(`RedisCacheMgr::setList: setting list with key ${listKey}`);
return this.set(listKey, listOfGetKeys);
}
async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
key = `${this.prefix}:${key}`;
log(`RedisCacheMgr::deepDel: choose direction ${direction}`);
if (direction === CacheDelDirection.CHILD_TO_PARENT) {
// given a child key, delete all keys in corresponding parent lists
const scopeList = await this.client.keys(`${this.prefix}:${scope}*list`);
for (const listKey of scopeList) {
// get target list
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (!list.length) {
continue;
}
// remove target Key
list = list.filter((k) => k !== key);
// delete list
log(`RedisCacheMgr::deepDel: remove listKey ${listKey}`);
await this.del(listKey);
if (list.length) {
// set target list
log(`RedisCacheMgr::deepDel: set key ${listKey}`);
await this.del(listKey);
await this.set(listKey, list);
}
}
log(`RedisCacheMgr::deepDel: remove key ${key}`);
return await this.del(key);
} else if (direction === CacheDelDirection.PARENT_TO_CHILD) {
// given a list key, delete all the children
const listOfChildren = await this.get(key, CacheGetType.TYPE_ARRAY);
// delete each child key
await Promise.all(listOfChildren.map(async (k) => await this.del(k)));
// delete list key
return await this.del(key);
} else {
log(`Invalid deepDel direction found : ${direction}`);
return Promise.resolve(false);
}
}
async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
list = [];
await this.del(listKey);
}
list.push(key);
return this.set(listKey, list);
}
async destroy(): Promise<boolean> {
log('RedisCacheMgr::destroy: destroy redis');
return this.client.flushdb();
}
async export(): Promise<any> {
log('RedisCacheMgr::export: export data');
const data = await this.client.keys('*');
const res = {};
return await Promise.all(
data.map(async (k) => {
res[k] = await this.get(
k,
k.slice(-4) === 'list'
? CacheGetType.TYPE_ARRAY
: CacheGetType.TYPE_OBJECT,
);
}),
).then(() => {
return res;
});
}
}

277
packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts vendored

@ -0,0 +1,277 @@
import debug from 'debug';
import Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');
export default class RedisMockCacheMgr extends CacheMgr {
client: any;
prefix: string;
constructor() {
super();
this.client = new Redis();
// flush the existing db with selected key (Default: 0)
this.client.flushdb();
// TODO(cache): fetch orgs once it's implemented
const orgs = 'noco';
this.prefix = `nc:${orgs}`;
}
// avoid circular structure to JSON
getCircularReplacer = () => {
const seen = new WeakSet();
return (_, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
// @ts-ignore
async del(key: string): Promise<any> {
log(`RedisMockCacheMgr::del: deleting key ${key}`);
return this.client.del(key);
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
} else if (type === CacheGetType.TYPE_OBJECT) {
const res = await this.client.get(key);
try {
const o = JSON.parse(res);
if (typeof o === 'object') {
if (
o &&
Object.keys(o).length === 0 &&
Object.getPrototypeOf(o) === Object.prototype
) {
log(`RedisMockCacheMgr::get: object is empty!`);
}
return Promise.resolve(o);
}
} catch (e) {
return Promise.resolve(res);
}
return Promise.resolve(res);
} else if (type === CacheGetType.TYPE_STRING) {
return await this.client.get(key);
}
log(`Invalid CacheGetType: ${type}`);
return Promise.resolve(false);
}
// @ts-ignore
async set(key: string, value: any): Promise<any> {
if (typeof value !== 'undefined' && value) {
log(`RedisMockCacheMgr::set: setting key ${key} with value ${value}`);
if (typeof value === 'object') {
if (Array.isArray(value) && value.length) {
return this.client.sadd(key, value);
}
return this.client.set(
key,
JSON.stringify(value, this.getCircularReplacer()),
);
}
return this.client.set(key, value);
} else {
log(`RedisMockCacheMgr::set: value is empty for ${key}. Skipping ...`);
return Promise.resolve(true);
}
}
// @ts-ignore
async getAll(pattern: string): Promise<any> {
return this.client.hgetall(pattern);
}
// @ts-ignore
async delAll(scope: string, pattern: string): Promise<any[]> {
// Example: nc:<orgs>:model:*:<id>
const keys = await this.client.keys(`${this.prefix}:${scope}:${pattern}`);
log(
`RedisMockCacheMgr::delAll: deleting all keys with pattern ${this.prefix}:${scope}:${pattern}`,
);
await Promise.all(
keys.map(
async (k) =>
await this.deepDel(scope, k, CacheDelDirection.CHILD_TO_PARENT),
),
);
return Promise.all(
keys.map(async (k) => {
await this.del(k);
}),
);
}
async getList(
scope: string,
subKeys: string[],
): Promise<{
list: any[];
isNoneList: boolean;
}> {
// remove null from arrays
subKeys = subKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const key =
subKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subKeys.join(':')}:list`;
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisMockCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE';
if (isNoneList) {
return Promise.resolve({
list: [],
isNoneList,
});
}
return {
list: await Promise.all(
arr.map(async (k) => await this.get(k, CacheGetType.TYPE_OBJECT)),
),
isNoneList,
};
}
async setList(
scope: string,
subListKeys: string[],
list: any[],
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// construct key for List
// e.g. nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
if (!list.length) {
// Set NONE here so that it won't hit the DB on each page load
return this.set(listKey, ['NONE']);
}
// fetch existing list
const listOfGetKeys =
(await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
for (const o of list) {
// construct key for Get
// e.g. nc:<orgs>:<scope>:<model_id_1>
let getKey = `${this.prefix}:${scope}:${o.id}`;
// special case - MODEL_ROLE_VISIBILITY
if (scope === CacheScope.MODEL_ROLE_VISIBILITY) {
getKey = `${this.prefix}:${scope}:${o.id}:${o.role}`;
}
// set Get Key
log(`RedisMockCacheMgr::setList: setting key ${getKey}`);
await this.set(getKey, JSON.stringify(o, this.getCircularReplacer()));
// push Get Key to List
listOfGetKeys.push(getKey);
}
// set List Key
log(`RedisMockCacheMgr::setList: setting list with key ${listKey}`);
return this.set(listKey, listOfGetKeys);
}
async deepDel(
scope: string,
key: string,
direction: string,
): Promise<boolean> {
key = `${this.prefix}:${key}`;
log(`RedisMockCacheMgr::deepDel: choose direction ${direction}`);
if (direction === CacheDelDirection.CHILD_TO_PARENT) {
// given a child key, delete all keys in corresponding parent lists
const scopeList = await this.client.keys(`${this.prefix}:${scope}*list`);
for (const listKey of scopeList) {
// get target list
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (!list.length) {
continue;
}
// remove target Key
list = list.filter((k) => k !== key);
// delete list
log(`RedisMockCacheMgr::deepDel: remove listKey ${listKey}`);
await this.del(listKey);
if (list.length) {
// set target list
log(`RedisMockCacheMgr::deepDel: set key ${listKey}`);
await this.del(listKey);
await this.set(listKey, list);
}
}
log(`RedisMockCacheMgr::deepDel: remove key ${key}`);
return await this.del(key);
} else if (direction === CacheDelDirection.PARENT_TO_CHILD) {
// given a list key, delete all the children
const listOfChildren = await this.get(key, CacheGetType.TYPE_ARRAY);
// delete each child key
await Promise.all(listOfChildren.map(async (k) => await this.del(k)));
// delete list key
return await this.del(key);
} else {
log(`Invalid deepDel direction found : ${direction}`);
return Promise.resolve(false);
}
}
async appendToList(
scope: string,
subListKeys: string[],
key: string,
): Promise<boolean> {
// remove null from arrays
subListKeys = subListKeys.filter((k) => k);
// e.g. key = nc:<orgs>:<scope>:<project_id_1>:<base_id_1>:list
const listKey =
subListKeys.length === 0
? `${this.prefix}:${scope}:list`
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisMockCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
list = [];
await this.del(listKey);
}
list.push(key);
return this.set(listKey, list);
}
async destroy(): Promise<boolean> {
log('RedisMockCacheMgr::destroy: destroy redis');
return this.client.flushdb();
}
async export(): Promise<any> {
log('RedisMockCacheMgr::export: export data');
const data = await this.client.keys('*');
const res = {};
return await Promise.all(
data.map(async (k) => {
res[k] = await this.get(
k,
k.slice(-4) === 'list'
? CacheGetType.TYPE_ARRAY
: CacheGetType.TYPE_OBJECT,
);
}),
).then(() => {
return res;
});
}
}

19
packages/nocodb-nest/src/connection/connection.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { Connection } from './knex';
import type { TestingModule } from '@nestjs/testing';
describe('Knex', () => {
let provider: Connection;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [Connection],
}).compile();
provider = module.get<Connection>(Connection);
});
it('should be defined', () => {
expect(provider).toBeDefined();
});
});

37
packages/nocodb-nest/src/connection/connection.ts

@ -0,0 +1,37 @@
import { Global, Injectable, Scope } from '@nestjs/common';
import { XKnex } from '../db/CustomKnex';
import NcConfigFactory from '../utils/NcConfigFactory';
import type * as knex from 'knex';
@Injectable({
scope: Scope.DEFAULT,
})
export class Connection {
public static knex: knex.Knex;
public static _config: any;
get knexInstance(): knex.Knex {
return Connection.knex;
}
get config(): knex.Knex {
return Connection._config;
}
// init metadb connection
static async init(): Promise<void> {
Connection._config = await NcConfigFactory.make();
if (!Connection.knex) {
Connection.knex = XKnex({
...this._config.meta.db,
useNullAsDefault: true,
});
}
}
// init metadb connection
async init(): Promise<void> {
return await Connection.init();
}
}

4
packages/nocodb-nest/src/constants/index.ts

@ -0,0 +1,4 @@
export const NC_LICENSE_KEY = 'nc-license-key';
export const NC_APP_SETTINGS = 'nc-app-settings';
export const NC_ATTACHMENT_FIELD_SIZE =
+process.env['NC_ATTACHMENT_FIELD_SIZE'] || 20 * 1024 * 1024; // 20 MB

21
packages/nocodb-nest/src/controllers/api-docs/api-docs.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { ApiDocsService } from '../../services/api-docs/api-docs.service';
import { ApiDocsController } from './api-docs.controller';
import type { TestingModule } from '@nestjs/testing';
describe('ApiDocsController', () => {
let controller: ApiDocsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ApiDocsController],
providers: [ApiDocsService],
}).compile();
controller = module.get<ApiDocsController>(ApiDocsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

43
packages/nocodb-nest/src/controllers/api-docs/api-docs.controller.ts

@ -0,0 +1,43 @@
import {
Controller,
Get,
Param,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { GlobalGuard } from '../../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware';
import { ApiDocsService } from '../../services/api-docs/api-docs.service';
import getSwaggerHtml from './template/swaggerHtml';
import getRedocHtml from './template/redocHtml';
@Controller()
export class ApiDocsController {
constructor(private readonly apiDocsService: ApiDocsService) {}
@Get('/api/v1/db/meta/projects/:projectId/swagger.json')
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
@Acl('swaggerJson')
async swaggerJson(@Param('projectId') projectId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJson({
projectId: projectId,
siteUrl: req.ncSiteUrl,
});
return swagger;
}
@Get('/api/v1/db/meta/projects/:projectId/swagger')
swaggerHtml(@Param('projectId') projectId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@Get('/api/v1/db/meta/projects/:projectId/redoc')
redocHtml(@Param('projectId') projectId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
}

93
packages/nocodb-nest/src/controllers/api-docs/template/redocHtml.ts vendored

@ -0,0 +1,93 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB API Documentation</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="${ncSiteUrl}/css/fonts.montserrat.css" rel="stylesheet">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="redoc"></div>
<script src="${ncSiteUrl}/js/redoc.standalone.min.js"></script>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
const xhttp = new XMLHttpRequest();
xhttp.open("GET", "./swagger.json");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xhttp.onload = function () {
const swaggerJson = this.responseText;
const swagger = JSON.parse(swaggerJson);
Redoc.init(swagger, {
scrollYOffset: 50
}, document.getElementById('redoc'))
};
xhttp.send();
</script>
<script>
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px')
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>`;

87
packages/nocodb-nest/src/controllers/api-docs/template/swaggerHtml.ts vendored

@ -0,0 +1,87 @@
export default ({
ncSiteUrl,
}: {
ncSiteUrl: string;
}): string => `<!DOCTYPE html>
<html>
<head>
<title>NocoDB : API Docs</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="${ncSiteUrl}/favicon.ico" />
<link rel="stylesheet" href="${ncSiteUrl}/css/swagger-ui-bundle.4.5.2.min.css"/>
<script src="${ncSiteUrl}/js/swagger-ui-bundle.4.5.2.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
let initialLocalStorage = {}
try {
initialLocalStorage = JSON.parse(localStorage.getItem('nocodb-gui-v2') || '{}');
} catch (e) {
console.error('Failed to parse local storage', e);
}
var xmlhttp = new XMLHttpRequest(); // new HttpRequest instance
xmlhttp.open("GET", "./swagger.json");
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlhttp.setRequestHeader("xc-auth", initialLocalStorage && initialLocalStorage.token);
xmlhttp.onload = function () {
const ui = SwaggerUIBundle({
// url: ,
spec: JSON.parse(xmlhttp.responseText),
dom_id: '#app',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})
}
xmlhttp.send();
console.log('%c🚀 We are Hiring!!! 🚀%c\\n%cJoin the forces http://careers.nocodb.com', 'color:#1348ba;font-size:3rem;padding:20px;', 'display:none', 'font-size:1.5rem;padding:20px');
const linkEl = document.createElement('a')
linkEl.setAttribute('href', "http://careers.nocodb.com")
linkEl.setAttribute('target', '_blank')
linkEl.setAttribute('class', 'we-are-hiring')
linkEl.innerHTML = '🚀 We are Hiring!!! 🚀'
const styleEl = document.createElement('style');
styleEl.innerHTML = \`
.we-are-hiring {
position: fixed;
bottom: 50px;
right: -250px;
opacity: 0;
background: orange;
border-radius: 4px;
padding: 19px;
z-index: 200;
text-decoration: none;
text-transform: uppercase;
color: black;
transition: 1s opacity, 1s right;
display: block;
font-weight: bold;
}
.we-are-hiring.active {
opacity: 1;
right:25px;
}
@media only screen and (max-width: 600px) {
.we-are-hiring {
display: none;
}
}
\`
document.body.appendChild(linkEl, document.body.firstChild)
document.body.appendChild(styleEl, document.body.firstChild)
setTimeout(() => linkEl.classList.add('active'), 2000)
</script>
</body>
</html>
`;

21
packages/nocodb-nest/src/controllers/api-tokens.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { ApiTokensService } from '../services/api-tokens.service';
import { ApiTokensController } from './api-tokens.controller';
import type { TestingModule } from '@nestjs/testing';
describe('ApiTokensController', () => {
let controller: ApiTokensController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ApiTokensController],
providers: [ApiTokensService],
}).compile();
controller = module.get<ApiTokensController>(ApiTokensController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

52
packages/nocodb-nest/src/controllers/api-tokens.controller.ts

@ -0,0 +1,52 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { ApiTokensService } from '../services/api-tokens.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class ApiTokensController {
constructor(private readonly apiTokensService: ApiTokensService) {}
@Get('/api/v1/db/meta/projects/:projectId/api-tokens')
@Acl('apiTokenList')
async apiTokenList(@Request() req) {
return new PagedResponseImpl(
await this.apiTokensService.apiTokenList({ userId: req['user'].id }),
);
}
@Post('/api/v1/db/meta/projects/:projectId/api-tokens')
@HttpCode(200)
@Acl('apiTokenCreate')
async apiTokenCreate(@Request() req, @Body() body) {
return await this.apiTokensService.apiTokenCreate({
tokenBody: body,
userId: req['user'].id,
});
}
@Delete('/api/v1/db/meta/projects/:projectId/api-tokens/:token')
@Acl('apiTokenDelete')
async apiTokenDelete(@Request() req, @Param('token') token: string) {
return await this.apiTokensService.apiTokenDelete({
token,
user: req['user'],
});
}
}

21
packages/nocodb-nest/src/controllers/attachments.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { AttachmentsService } from '../services/attachments.service';
import { AttachmentsController } from './attachments.controller';
import type { TestingModule } from '@nestjs/testing';
describe('AttachmentsController', () => {
let controller: AttachmentsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AttachmentsController],
providers: [AttachmentsService],
}).compile();
controller = module.get<AttachmentsController>(AttachmentsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

100
packages/nocodb-nest/src/controllers/attachments.controller.ts

@ -0,0 +1,100 @@
import path from 'path';
import {
Body,
Controller,
Get,
HttpCode,
Param,
Post,
Query,
Request,
Response,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { GlobalGuard } from '../guards/global/global.guard';
import { UploadAllowedInterceptor } from '../interceptors/is-upload-allowed/is-upload-allowed.interceptor';
import { AttachmentsService } from '../services/attachments.service';
@Controller()
export class AttachmentsController {
constructor(private readonly attachmentsService: AttachmentsService) {}
@UseGuards(GlobalGuard)
@Post('/api/v1/db/storage/upload')
@HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor())
async upload(
@UploadedFiles() files: Array<any>,
@Body() body: any,
@Request() req: any,
@Query('path') path: string,
) {
const attachments = await this.attachmentsService.upload({
files: files,
path: req.query?.path as string,
});
return attachments;
}
@Post('/api/v1/db/storage/upload-by-url')
@HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor)
@UseGuards(GlobalGuard)
async uploadViaURL(@Body() body: any, @Query('path') path: string) {
const attachments = await this.attachmentsService.uploadViaURL({
urls: body,
path,
});
return attachments;
}
// @Get(/^\/download\/(.+)$/)
// , getCacheMiddleware(), catchError(fileRead));
@Get('/download/:filename(*)')
// This route will match any URL that starts with
async fileRead(@Param('filename') filename: string, @Response() res) {
try {
const { img, type } = await this.attachmentsService.fileRead({
path: path.join('nc', 'uploads', filename),
});
res.writeHead(200, { 'Content-Type': type });
res.end(img, 'binary');
} catch (e) {
console.log(e);
res.status(404).send('Not found');
}
}
// @Get(/^\/dl\/([^/]+)\/([^/]+)\/(.+)$/)
@Get('/dl/:param1([a-zA-Z0-9_-]+)/:param2([a-zA-Z0-9_-]+)/:filename(*)')
// getCacheMiddleware(),
async fileReadv2(
@Param('param1') param1: string,
@Param('param2') param2: string,
@Param('filename') filename: string,
@Response() res,
) {
try {
const { img, type } = await this.attachmentsService.fileRead({
path: path.join(
'nc',
param1,
param2,
'uploads',
...filename.split('/'),
),
});
res.writeHead(200, { 'Content-Type': type });
res.end(img, 'binary');
} catch (e) {
res.status(404).send('Not found');
}
}
}

21
packages/nocodb-nest/src/controllers/audits.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { AuditsService } from '../services/audits.service';
import { AuditsController } from './audits.controller';
import type { TestingModule } from '@nestjs/testing';
describe('AuditsController', () => {
let controller: AuditsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuditsController],
providers: [AuditsService],
}).compile();
controller = module.get<AuditsController>(AuditsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

96
packages/nocodb-nest/src/controllers/audits.controller.ts

@ -0,0 +1,96 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Patch,
Post,
Query,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { Audit } from '../models';
import { AuditsService } from '../services/audits.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class AuditsController {
constructor(private readonly auditsService: AuditsService) {}
@Post('/api/v1/db/meta/audits/comments')
@HttpCode(200)
@Acl('commentRow')
async commentRow(@Request() req) {
return await this.auditsService.commentRow({
user: (req as any).user,
body: req.body,
});
}
@Post('/api/v1/db/meta/audits/rows/:rowId/update')
@HttpCode(200)
@Acl('auditRowUpdate')
async auditRowUpdate(@Param('rowId') rowId: string, @Body() body: any) {
return await this.auditsService.auditRowUpdate({
rowId,
body,
});
}
@Get('/api/v1/db/meta/audits/comments')
@Acl('commentList')
async commentList(@Request() req) {
return new PagedResponseImpl(
await this.auditsService.commentList({ query: req.query }),
);
}
@Patch('/api/v1/db/meta/audits/:auditId/comment')
@Acl('commentUpdate')
async commentUpdate(
@Param('auditId') auditId: string,
@Request() req,
@Body() body: any,
) {
return await this.auditsService.commentUpdate({
auditId,
userEmail: req.user?.email,
body: body,
});
}
@Get('/api/v1/db/meta/projects/:projectId/audits')
@Acl('auditList')
async auditList(@Request() req, @Param('projectId') projectId: string) {
return new PagedResponseImpl(
await this.auditsService.auditList({
query: req.query,
projectId,
}),
{
count: await Audit.projectAuditCount(projectId),
...req.query,
},
);
}
@Get('/api/v1/db/meta/audits/comments/count')
@Acl('commentsCount')
async commentsCount(
@Query('fk_model_id') fk_model_id: string,
@Query('ids') ids: string[],
) {
return await this.auditsService.commentsCount({
fk_model_id,
ids,
});
}
}

21
packages/nocodb-nest/src/controllers/auth.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { AuthService } from '../services/auth.service';
import { AuthController } from './auth.controller';
import type { TestingModule } from '@nestjs/testing';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [AuthService],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

48
packages/nocodb-nest/src/controllers/auth.controller.ts

@ -0,0 +1,48 @@
import {
Body,
Controller,
Get,
HttpCode,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { ExtractProjectIdMiddleware } from '../middlewares/extract-project-id/extract-project-id.middleware';
import extractRolesObj from '../utils/extractRolesObj';
import { AuthService } from '../services/auth.service';
export class CreateUserDto {
readonly username: string;
readonly email: string;
readonly password: string;
}
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post('/api/v1/auth/user/signin')
@HttpCode(200)
async signin(@Request() req) {
return this.authService.login(req.user);
}
@Post('/api/v1/auth/user/signup')
@HttpCode(200)
async signup(@Body() createUserDto: CreateUserDto) {
return await this.authService.signup(createUserDto);
}
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
@Get('/api/v1/auth/user/me')
async me(@Request() req) {
const user = {
...req.user,
roles: extractRolesObj(req.user.roles),
};
return user;
}
}

21
packages/nocodb-nest/src/controllers/bases.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { BasesService } from '../services/bases.service';
import { BasesController } from './bases.controller';
import type { TestingModule } from '@nestjs/testing';
describe('BasesController', () => {
let controller: BasesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BasesController],
providers: [BasesService],
}).compile();
controller = module.get<BasesController>(BasesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

89
packages/nocodb-nest/src/controllers/bases.controller.ts

@ -0,0 +1,89 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { BaseReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { BasesService } from '../services/bases.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class BasesController {
constructor(private readonly basesService: BasesService) {}
@Get('/api/v1/db/meta/projects/:projectId/bases/:baseId')
@Acl('baseGet')
async baseGet(@Param('baseId') baseId: string) {
const base = await this.basesService.baseGetWithConfig({
baseId,
});
return base;
}
@Patch('/api/v1/db/meta/projects/:projectId/bases/:baseId')
@Acl('baseUpdate')
async baseUpdate(
@Param('baseId') baseId: string,
@Param('projectId') projectId: string,
@Body() body: BaseReqType,
) {
const base = await this.basesService.baseUpdate({
baseId,
base: body,
projectId,
});
return base;
}
@Get('/api/v1/db/meta/projects/:projectId/bases')
@Acl('baseList')
async baseList(@Param('projectId') projectId: string) {
const bases = await this.basesService.baseList({
projectId,
});
return new PagedResponseImpl(bases, {
count: bases.length,
limit: bases.length,
});
}
@Delete('/api/v1/db/meta/projects/:projectId/bases/:baseId')
@Acl('baseDelete')
async baseDelete(@Param('baseId') baseId: string) {
const result = await this.basesService.baseDelete({
baseId,
});
return result;
}
@Post('/api/v1/db/meta/projects/:projectId/bases')
@HttpCode(200)
@Acl('baseCreate')
async baseCreate(
@Param('projectId') projectId: string,
@Body() body: BaseReqType,
) {
const base = await this.basesService.baseCreate({
projectId,
base: body,
});
return base;
}
}

19
packages/nocodb-nest/src/controllers/bulk-data-alias.controller.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { BulkDataAliasController } from './bulk-data-alias.controller';
import type { TestingModule } from '@nestjs/testing';
describe('BulkDataAliasController', () => {
let controller: BulkDataAliasController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BulkDataAliasController],
}).compile();
controller = module.get<BulkDataAliasController>(BulkDataAliasController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

112
packages/nocodb-nest/src/controllers/bulk-data-alias.controller.ts

@ -0,0 +1,112 @@
import {
Body,
Controller,
Delete,
HttpCode,
Param,
Patch,
Post,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { BulkDataAliasService } from '../services/bulk-data-alias.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class BulkDataAliasController {
constructor(private bulkDataAliasService: BulkDataAliasService) {}
@Post('/api/v1/db/data/bulk/:orgs/:projectName/:tableName')
@HttpCode(200)
@Acl('bulkDataInsert')
async bulkDataInsert(
@Request() req,
@Response() res,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Body() body: any,
) {
const exists = await this.bulkDataAliasService.bulkDataInsert({
body: body,
cookie: req,
projectName: projectName,
tableName: tableName,
});
res.json(exists);
}
@Patch('/api/v1/db/data/bulk/:orgs/:projectName/:tableName')
@Acl('bulkDataUpdate')
async bulkDataUpdate(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Body() body: any,
) {
return await this.bulkDataAliasService.bulkDataUpdate({
body: body,
cookie: req,
projectName: projectName,
tableName: tableName,
});
}
// todo: Integrate with filterArrJson bulkDataUpdateAll
@Patch('/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all')
@Acl('bulkDataUpdateAll')
async bulkDataUpdateAll(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Body() body: any,
) {
return await this.bulkDataAliasService.bulkDataUpdateAll({
body: body,
cookie: req,
projectName: projectName,
tableName: tableName,
query: req.query,
});
}
@Delete('/api/v1/db/data/bulk/:orgs/:projectName/:tableName')
@Acl('bulkDataDelete')
async bulkDataDelete(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Body() body: any,
) {
return await this.bulkDataAliasService.bulkDataDelete({
body: body,
cookie: req,
projectName: projectName,
tableName: tableName,
});
}
// todo: Integrate with filterArrJson bulkDataDeleteAll
@Delete('/api/v1/db/data/bulk/:orgs/:projectName/:tableName/all')
@Acl('bulkDataDeleteAll')
async bulkDataDeleteAll(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.bulkDataAliasService.bulkDataDeleteAll({
// cookie: req,
projectName: projectName,
tableName: tableName,
query: req.query,
});
}
}

21
packages/nocodb-nest/src/controllers/caches.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { CachesService } from '../services/caches.service';
import { CachesController } from './caches.controller';
import type { TestingModule } from '@nestjs/testing';
describe('CachesController', () => {
let controller: CachesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CachesController],
providers: [CachesService],
}).compile();
controller = module.get<CachesController>(CachesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

25
packages/nocodb-nest/src/controllers/caches.controller.ts

@ -0,0 +1,25 @@
import { Controller, Delete, Get } from '@nestjs/common';
import { Acl } from '../middlewares/extract-project-id/extract-project-id.middleware';
import { CachesService } from '../services/caches.service';
@Controller()
export class CachesController {
constructor(private readonly cachesService: CachesService) {}
@Get('/api/v1/db/meta/cache')
@Acl('cacheGet')
async cacheGet(_, res) {
const data = await this.cachesService.cacheGet();
res.set({
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="cache-export.json"`,
});
return JSON.stringify(data);
}
@Delete('/api/v1/db/meta/cache')
@Acl('cacheDelete')
async cacheDelete() {
return await this.cachesService.cacheDelete();
}
}

21
packages/nocodb-nest/src/controllers/columns.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { ColumnsService } from '../services/columns.service';
import { ColumnsController } from './columns.controller';
import type { TestingModule } from '@nestjs/testing';
describe('ColumnsController', () => {
let controller: ColumnsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ColumnsController],
providers: [ColumnsService],
}).compile();
controller = module.get<ColumnsController>(ColumnsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

74
packages/nocodb-nest/src/controllers/columns.controller.ts

@ -0,0 +1,74 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { ColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { ColumnsService } from '../services/columns.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class ColumnsController {
constructor(private readonly columnsService: ColumnsService) {}
@Post('/api/v1/db/meta/tables/:tableId/columns/')
@HttpCode(200)
@Acl('columnAdd')
async columnAdd(
@Param('tableId') tableId: string,
@Body() body: ColumnReqType,
@Request() req: any,
) {
return await this.columnsService.columnAdd({
tableId,
column: body,
req,
});
}
@Patch('/api/v1/db/meta/columns/:columnId')
@Acl('columnUpdate')
async columnUpdate(
@Param('columnId') columnId: string,
@Body() body: ColumnReqType,
@Request() req: any,
) {
return await this.columnsService.columnUpdate({
columnId: columnId,
column: body,
req,
});
}
@Delete('/api/v1/db/meta/columns/:columnId')
@Acl('columnDelete')
async columnDelete(@Param('columnId') columnId: string, @Request() req: any) {
return await this.columnsService.columnDelete({ columnId, req });
}
@Get('/api/v1/db/meta/columns/:columnId')
@Acl('columnGet')
async columnGet(@Param('columnId') columnId: string) {
return await this.columnsService.columnGet({ columnId });
}
@Post('/api/v1/db/meta/columns/:columnId/primary')
@HttpCode(200)
@Acl('columnSetAsPrimary')
async columnSetAsPrimary(@Param('columnId') columnId: string) {
return await this.columnsService.columnSetAsPrimary({ columnId });
}
}

21
packages/nocodb-nest/src/controllers/data-alias-export.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { DataAliasExportController } from './data-alias-export.controller';
import type { TestingModule } from '@nestjs/testing';
describe('DataAliasExportController', () => {
let controller: DataAliasExportController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DataAliasExportController],
}).compile();
controller = module.get<DataAliasExportController>(
DataAliasExportController,
);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

68
packages/nocodb-nest/src/controllers/data-alias-export.controller.ts

@ -0,0 +1,68 @@
import { Controller, Get, Request, Response, UseGuards } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { View } from '../models';
import { DatasService } from '../services/datas.service';
import { extractCsvData, extractXlsxData } from '../modules/datas/helpers';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DataAliasExportController {
constructor(private datasService: DatasService) {}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/export/excel',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/export/excel',
])
@Acl('exportExcel')
async excelDataExport(@Request() req, @Response() res) {
const { model, view } =
await this.datasService.getViewAndModelFromRequestByAliasOrId(req);
let targetView = view;
if (!targetView) {
targetView = await View.getDefaultView(model.id);
}
const { offset, elapsed, data } = await extractXlsxData(targetView, req);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, data, targetView.title);
const buf = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' });
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
targetView.title,
)}-export.xlsx"`,
});
res.end(buf);
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/export/csv',
'/api/v1/db/data/:orgs/:projectName/:tableName/export/csv',
])
@Acl('exportCsv')
async csvDataExport(@Request() req, @Response() res) {
const { model, view } =
await this.datasService.getViewAndModelFromRequestByAliasOrId(req);
let targetView = view;
if (!targetView) {
targetView = await View.getDefaultView(model.id);
}
const { offset, elapsed, data } = await extractCsvData(targetView, req);
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,
'nc-export-elapsed-time': elapsed,
'Content-Disposition': `attachment; filename="${encodeURI(
targetView.title,
)}-export.csv"`,
});
res.send(data);
}
}

21
packages/nocodb-nest/src/controllers/data-alias-nested.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { DataAliasNestedController } from './data-alias-nested.controller';
import type { TestingModule } from '@nestjs/testing';
describe('DataAliasNestedController', () => {
let controller: DataAliasNestedController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DataAliasNestedController],
}).compile();
controller = module.get<DataAliasNestedController>(
DataAliasNestedController,
);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

174
packages/nocodb-nest/src/controllers/data-alias-nested.controller.ts

@ -0,0 +1,174 @@
import {
Controller,
Delete,
Get,
HttpCode,
Param,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { DataAliasNestedService } from '../services/data-alias-nested.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DataAliasNestedController {
constructor(private dataAliasNestedService: DataAliasNestedService) {}
// todo: handle case where the given column is not ltar
@Get('/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName')
@Acl('mmList')
async mmList(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.mmList({
query: req.query,
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
});
}
@Get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/mm/:columnName/exclude',
)
@Acl('mmExcludedList')
async mmExcludedList(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.mmExcludedList({
query: req.query,
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
});
}
@Get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName/exclude',
)
@Acl('hmExcludedList')
async hmExcludedList(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.hmExcludedList({
query: req.query,
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
});
}
@Get(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/bt/:columnName/exclude',
)
@Acl('btExcludedList')
async btExcludedList(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.btExcludedList({
query: req.query,
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
});
}
// todo: handle case where the given column is not ltar
@Get('/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/hm/:columnName')
@Acl('hmList')
async hmList(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
) {
return await this.dataAliasNestedService.hmList({
query: req.query,
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
});
}
@Delete(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId',
)
@Acl('relationDataRemove')
async relationDataRemove(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataRemove({
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
cookie: req,
refRowId: refRowId,
});
return { msg: 'The relation data has been deleted successfully' };
}
// todo: Give proper error message when reference row is already related and handle duplicate ref row id in hm
@Post(
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/:relationType/:columnName/:refRowId',
)
@Acl('relationDataAdd')
@HttpCode(200)
async relationDataAdd(
@Request() req,
@Param('columnName') columnName: string,
@Param('rowId') rowId: string,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataAdd({
columnName: columnName,
rowId: rowId,
projectName: projectName,
tableName: tableName,
cookie: req,
refRowId: refRowId,
});
return { msg: 'The relation data has been created successfully' };
}
}

19
packages/nocodb-nest/src/controllers/data-alias-nested.service.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { DataAliasNestedService } from '../services/data-alias-nested.service';
import type { TestingModule } from '@nestjs/testing';
describe('DataAliasNestedService', () => {
let service: DataAliasNestedService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DataAliasNestedService],
}).compile();
service = module.get<DataAliasNestedService>(DataAliasNestedService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

21
packages/nocodb-nest/src/controllers/data-alias.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { DatasService } from '../services/datas.service';
import { DataAliasController } from './data-alias.controller';
import type { TestingModule } from '@nestjs/testing';
describe('DataAliasController', () => {
let controller: DataAliasController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DataAliasController],
providers: [DatasService],
}).compile();
controller = module.get<DataAliasController>(DataAliasController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

250
packages/nocodb-nest/src/controllers/data-alias.controller.ts

@ -0,0 +1,250 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { parseHrtimeToSeconds } from '../helpers';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { DatasService } from '../services/datas.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DataAliasController {
constructor(private readonly datasService: DatasService) {}
// todo: Handle the error case where view doesnt belong to model
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName',
])
@Acl('dataList')
async dataList(
@Request() req,
@Response() res,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
) {
const startTime = process.hrtime();
const responseData = await this.datasService.dataList({
query: req.query,
projectName: projectName,
tableName: tableName,
viewName: viewName,
});
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(responseData);
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/find-one',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/find-one',
])
@Acl('dataFindOne')
async dataFindOne(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
) {
return await this.datasService.dataFindOne({
query: req.query,
projectName: projectName,
tableName: tableName,
viewName: viewName,
});
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/groupby',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/groupby',
])
@Acl('dataGroupBy')
async dataGroupBy(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
) {
return await this.datasService.dataGroupBy({
query: req.query,
projectName: projectName,
tableName: tableName,
viewName: viewName,
});
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/count',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/count',
])
@Acl('dataCount')
async dataCount(
@Request() req,
@Response() res,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
) {
const countResult = await this.datasService.dataCount({
query: req.query,
projectName: projectName,
tableName: tableName,
viewName: viewName,
});
res.json(countResult);
}
@Post([
'/api/v1/db/data/:orgs/:projectName/:tableName',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName',
])
@HttpCode(200)
@Acl('dataInsert')
async dataInsert(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Body() body: any,
) {
return await this.datasService.dataInsert({
projectName: projectName,
tableName: tableName,
viewName: viewName,
body: body,
cookie: req,
});
}
@Patch([
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
])
@Acl('dataUpdate')
async dataUpdate(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.dataUpdate({
projectName: projectName,
tableName: tableName,
viewName: viewName,
body: req.body,
cookie: req,
rowId: rowId,
});
}
@Delete([
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
])
@Acl('dataDelete')
async dataDelete(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.dataDelete({
projectName: projectName,
tableName: tableName,
viewName: viewName,
cookie: req,
rowId: rowId,
});
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId',
])
@Acl('dataRead')
async dataRead(
@Request() req,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.dataRead({
projectName: projectName,
tableName: tableName,
viewName: viewName,
rowId: rowId,
query: req.query,
});
}
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/:rowId/exist',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/:rowId/exist',
])
@Acl('dataExist')
async dataExist(
@Request() req,
@Response() res,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Param('rowId') rowId: string,
) {
const exists = await this.datasService.dataExist({
projectName: projectName,
tableName: tableName,
viewName: viewName,
rowId: rowId,
query: req.query,
});
res.json(exists);
}
// todo: Handle the error case where view doesnt belong to model
@Get([
'/api/v1/db/data/:orgs/:projectName/:tableName/group/:columnId',
'/api/v1/db/data/:orgs/:projectName/:tableName/views/:viewName/group/:columnId',
])
@Acl('groupedDataList')
async groupedDataList(
@Request() req,
@Response() res,
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('viewName') viewName: string,
@Param('columnId') columnId: string,
) {
const startTime = process.hrtime();
const groupedData = await this.datasService.groupedDataList({
projectName: projectName,
tableName: tableName,
viewName: viewName,
query: req.query,
columnId: columnId,
});
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.json(groupedData);
}
}

21
packages/nocodb-nest/src/controllers/datas.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { DatasService } from '../services/datas.service';
import { DatasController } from './datas.controller';
import type { TestingModule } from '@nestjs/testing';
describe('DatasController', () => {
let controller: DatasController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DatasController],
providers: [DatasService],
}).compile();
controller = module.get<DatasController>(DatasController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

215
packages/nocodb-nest/src/controllers/datas.controller.ts

@ -0,0 +1,215 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { DatasService } from '../services/datas.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class DatasController {
constructor(private readonly datasService: DatasService) {}
@Get('/data/:viewId/')
@Acl('dataList')
async dataList(@Request() req, @Param('viewId') viewId: string) {
return await this.datasService.dataListByViewId({
viewId: viewId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId/mm/:colId')
@Acl('mmList')
async mmList(
@Request() req,
@Param('viewId') viewId: string,
@Param('colId') colId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.mmList({
viewId: viewId,
colId: colId,
rowId: rowId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId/mm/:colId/exclude')
@Acl('mmExcludedList')
async mmExcludedList(
@Request() req,
@Param('viewId') viewId: string,
@Param('colId') colId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.mmExcludedList({
viewId: viewId,
colId: colId,
rowId: rowId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId/hm/:colId/exclude')
@Acl('hmExcludedList')
async hmExcludedList(
@Request() req,
@Param('viewId') viewId: string,
@Param('colId') colId: string,
@Param('rowId') rowId: string,
) {
await this.datasService.hmExcludedList({
viewId: viewId,
colId: colId,
rowId: rowId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId/bt/:colId/exclude')
@Acl('btExcludedList')
async btExcludedList(
@Request() req,
@Param('viewId') viewId: string,
@Param('colId') colId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.btExcludedList({
viewId: viewId,
colId: colId,
rowId: rowId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId/hm/:colId')
@Acl('hmList')
async hmList(
@Request() req,
@Param('viewId') viewId: string,
@Param('colId') colId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.hmList({
viewId: viewId,
colId: colId,
rowId: rowId,
query: req.query,
});
}
@Get('/data/:viewId/:rowId')
@Acl('dataRead')
async dataRead(
@Request() req,
@Param('viewId') viewId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.dataReadByViewId({
viewId,
rowId,
query: req.query,
});
}
@Post('/data/:viewId/')
@HttpCode(200)
@Acl('dataInsert')
async dataInsert(
@Request() req,
@Param('viewId') viewId: string,
@Body() body: any,
) {
return await this.datasService.dataInsertByViewId({
viewId: viewId,
body: body,
cookie: req,
});
}
@Patch('/data/:viewId/:rowId')
@Acl('dataUpdate')
async dataUpdate(
@Request() req,
@Param('viewId') viewId: string,
@Param('rowId') rowId: string,
@Body() body: any,
) {
return await this.datasService.dataUpdateByViewId({
viewId: viewId,
rowId: rowId,
body: body,
cookie: req,
});
}
@Delete('/data/:viewId/:rowId')
@Acl('dataDelete')
async dataDelete(
@Request() req,
@Param('viewId') viewId: string,
@Param('rowId') rowId: string,
) {
return await this.datasService.dataDeleteByViewId({
viewId: viewId,
rowId: rowId,
cookie: req,
});
}
@Delete('/data/:viewId/:rowId/:relationType/:colId/:childId')
@Acl('relationDataDelete')
async relationDataDelete(
@Request() req,
@Param('viewId') viewId: string,
@Param('rowId') rowId: string,
@Param('relationType') relationType: string,
@Param('colId') colId: string,
@Param('childId') childId: string,
) {
await this.datasService.relationDataDelete({
viewId: viewId,
colId: colId,
childId: childId,
rowId: rowId,
cookie: req,
});
return { msg: 'The relation data has been deleted successfully' };
}
@Post('/data/:viewId/:rowId/:relationType/:colId/:childId')
@HttpCode(200)
@Acl('relationDataAdd')
async relationDataAdd(
@Request() req,
@Param('viewId') viewId: string,
@Param('rowId') rowId: string,
@Param('relationType') relationType: string,
@Param('colId') colId: string,
@Param('childId') childId: string,
) {
await this.datasService.relationDataAdd({
viewId: viewId,
colId: colId,
childId: childId,
rowId: rowId,
cookie: req,
});
return { msg: 'The relation data has been created successfully' };
}
}

21
packages/nocodb-nest/src/controllers/filters.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { FiltersService } from '../services/filters.service';
import { FiltersController } from './filters.controller';
import type { TestingModule } from '@nestjs/testing';
describe('FiltersController', () => {
let controller: FiltersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FiltersController],
providers: [FiltersService],
}).compile();
controller = module.get<FiltersController>(FiltersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

113
packages/nocodb-nest/src/controllers/filters.controller.ts

@ -0,0 +1,113 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FilterReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
UseAclMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FiltersService } from '../services/filters.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class FiltersController {
constructor(private readonly filtersService: FiltersService) {}
@Get('/api/v1/db/meta/views/:viewId/filters')
@Acl('filterList')
async filterList(@Param('viewId') viewId: string) {
return new PagedResponseImpl(
await this.filtersService.filterList({
viewId,
}),
);
}
@Post('/api/v1/db/meta/views/:viewId/filters')
@HttpCode(200)
@Acl('filterCreate')
async filterCreate(
@Param('viewId') viewId: string,
@Body() body: FilterReqType,
) {
const filter = await this.filtersService.filterCreate({
filter: body,
viewId: viewId,
});
return filter;
}
@Post('/api/v1/db/meta/hooks/:hookId/filters')
@HttpCode(200)
@Acl('hookFilterCreate')
async hookFilterCreate(
@Param('hookId') hookId: string,
@Body() body: FilterReqType,
) {
const filter = await this.filtersService.hookFilterCreate({
filter: body,
hookId,
});
return filter;
}
@Get('/api/v1/db/meta/filters/:filterId')
@Acl('filterGet')
async filterGet(@Param('filterId') filterId: string) {
return await this.filtersService.filterGet({ filterId });
}
@Get('/api/v1/db/meta/filters/:filterParentId/children')
@Acl('filterChildrenList')
async filterChildrenRead(filterParentId: string) {
return new PagedResponseImpl(
await this.filtersService.filterChildrenList({
filterId: filterParentId,
}),
);
}
@Patch('/api/v1/db/meta/filters/:filterId')
@Acl('filterUpdate')
async filterUpdate(
@Param('filterId') filterId: string,
@Body() body: FilterReqType,
) {
const filter = await this.filtersService.filterUpdate({
filterId: filterId,
filter: body,
});
return filter;
}
@Delete('/api/v1/db/meta/filters/:filterId')
@Acl('filterDelete')
async filterDelete(@Param('filterId') filterId: string) {
const filter = await this.filtersService.filterDelete({
filterId,
});
return filter;
}
@Get('/api/v1/db/meta/hooks/:hookId/filters')
@Acl('hookFilterList')
async hookFilterList(@Param('hookId') hookId: string) {
return new PagedResponseImpl(
await this.filtersService.hookFilterList({
hookId: hookId,
}),
);
}
}

21
packages/nocodb-nest/src/controllers/form-columns.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { FormColumnsService } from '../services/form-columns.service';
import { FormColumnsController } from './form-columns.controller';
import type { TestingModule } from '@nestjs/testing';
describe('FormColumnsController', () => {
let controller: FormColumnsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FormColumnsController],
providers: [FormColumnsService],
}).compile();
controller = module.get<FormColumnsController>(FormColumnsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

28
packages/nocodb-nest/src/controllers/form-columns.controller.ts

@ -0,0 +1,28 @@
import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FormColumnsService } from '../services/form-columns.service';
class FormColumnUpdateReqType {}
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class FormColumnsController {
constructor(private readonly formColumnsService: FormColumnsService) {}
@Patch('/api/v1/db/meta/form-columns/:formViewColumnId')
@Acl('columnUpdate')
async columnUpdate(
@Param('formViewColumnId') formViewColumnId: string,
@Body() formViewColumnbody: FormColumnUpdateReqType,
) {
return await this.formColumnsService.columnUpdate({
formViewColumnId,
formViewColumn: formViewColumnbody,
});
}
}

21
packages/nocodb-nest/src/controllers/forms.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { FormsService } from '../services/forms.service';
import { FormsController } from './forms.controller';
import type { TestingModule } from '@nestjs/testing';
describe('FormsController', () => {
let controller: FormsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FormsController],
providers: [FormsService],
}).compile();
controller = module.get<FormsController>(FormsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

54
packages/nocodb-nest/src/controllers/forms.controller.ts

@ -0,0 +1,54 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FormsService } from '../services/forms.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class FormsController {
constructor(private readonly formsService: FormsService) {}
@Get('/api/v1/db/meta/forms/:formViewId')
@Acl('formViewGet')
async formViewGet(@Param('formViewId') formViewId: string) {
const formViewData = await this.formsService.formViewGet({
formViewId,
});
return formViewData;
}
@Post('/api/v1/db/meta/tables/:tableId/forms')
@HttpCode(200)
@Acl('formViewCreate')
async formViewCreate(
@Param('tableId') tableId: string,
@Body() body: ViewCreateReqType,
) {
const view = await this.formsService.formViewCreate({
body,
tableId,
});
return view;
}
@Patch('/api/v1/db/meta/forms/:formViewId')
@Acl('formViewUpdate')
async formViewUpdate(@Param('formViewId') formViewId: string, @Body() body) {
return await this.formsService.formViewUpdate({
formViewId,
form: body,
});
}
}

21
packages/nocodb-nest/src/controllers/galleries.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { GalleriesService } from '../services/galleries.service';
import { GalleriesController } from './galleries.controller';
import type { TestingModule } from '@nestjs/testing';
describe('GalleriesController', () => {
let controller: GalleriesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GalleriesController],
providers: [GalleriesService],
}).compile();
controller = module.get<GalleriesController>(GalleriesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

58
packages/nocodb-nest/src/controllers/galleries.controller.ts

@ -0,0 +1,58 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { GalleriesService } from '../services/galleries.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class GalleriesController {
constructor(private readonly galleriesService: GalleriesService) {}
@Get('/api/v1/db/meta/galleries/:galleryViewId')
@Acl('galleryViewGet')
async galleryViewGet(@Param('galleryViewId') galleryViewId: string) {
return await this.galleriesService.galleryViewGet({
galleryViewId,
});
}
@Post('/api/v1/db/meta/tables/:tableId/galleries')
@HttpCode(200)
@Acl('galleryViewCreate')
async galleryViewCreate(
@Param('tableId') tableId: string,
@Body() body: ViewCreateReqType,
) {
return await this.galleriesService.galleryViewCreate({
gallery: body,
// todo: sanitize
tableId,
});
}
@Patch('/api/v1/db/meta/galleries/:galleryViewId')
@Acl('galleryViewUpdate')
async galleryViewUpdate(
@Param('galleryViewId') galleryViewId: string,
@Body() body: GalleryUpdateReqType,
) {
return await this.galleriesService.galleryViewUpdate({
galleryViewId,
gallery: body,
});
}
}

21
packages/nocodb-nest/src/controllers/grid-columns.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { GridColumnsService } from '../services/grid-columns.service';
import { GridColumnsController } from './grid-columns.controller';
import type { TestingModule } from '@nestjs/testing';
describe('GridColumnsController', () => {
let controller: GridColumnsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GridColumnsController],
providers: [GridColumnsService],
}).compile();
controller = module.get<GridColumnsController>(GridColumnsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

34
packages/nocodb-nest/src/controllers/grid-columns.controller.ts

@ -0,0 +1,34 @@
import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { GridColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { GridColumnsService } from '../services/grid-columns.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class GridColumnsController {
constructor(private readonly gridColumnsService: GridColumnsService) {}
@Get('/api/v1/db/meta/grids/:gridViewId/grid-columns')
@Acl('columnList')
async columnList(@Param('gridViewId') gridViewId: string) {
return await this.gridColumnsService.columnList({
gridViewId,
});
}
@Patch('/api/v1/db/meta/grid-columns/:gridViewColumnId')
@Acl('gridColumnUpdate')
async gridColumnUpdate(
@Param('gridViewColumnId') gridViewColumnId: string,
@Body() body: GridColumnReqType,
) {
return this.gridColumnsService.gridColumnUpdate({
gridViewColumnId,
grid: body,
});
}
}

21
packages/nocodb-nest/src/controllers/grids.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { GridsService } from '../services/grids.service';
import { GridsController } from './grids.controller';
import type { TestingModule } from '@nestjs/testing';
describe('GridsController', () => {
let controller: GridsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GridsController],
providers: [GridsService],
}).compile();
controller = module.get<GridsController>(GridsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

47
packages/nocodb-nest/src/controllers/grids.controller.ts

@ -0,0 +1,47 @@
import {
Body,
Controller,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { GridsService } from '../services/grids.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class GridsController {
get '/api/v1/db/meta/tables/:tableId/grids/'() {
return this['_/api/v1/db/meta/tables/:tableId/grids/'];
}
constructor(private readonly gridsService: GridsService) {}
@Post('/api/v1/db/meta/tables/:tableId/grids/')
@HttpCode(200)
@Acl('gridViewCreate')
async gridViewCreate(
@Param('tableId') tableId: string,
@Body() body: ViewCreateReqType,
) {
const view = await this.gridsService.gridViewCreate({
grid: body,
tableId,
});
return view;
}
@Patch('/api/v1/db/meta/grids/:viewId')
async gridViewUpdate(@Param('viewId') viewId: string, @Body() body) {
return await this.gridsService.gridViewUpdate({
viewId,
grid: body,
});
}
}

21
packages/nocodb-nest/src/controllers/hooks.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { HooksService } from '../services/hooks.service';
import { HooksController } from './hooks.controller';
import type { TestingModule } from '@nestjs/testing';
describe('HooksController', () => {
let controller: HooksController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HooksController],
providers: [HooksService],
}).compile();
controller = module.get<HooksController>(HooksController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

114
packages/nocodb-nest/src/controllers/hooks.controller.ts

@ -0,0 +1,114 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { HookReqType, HookTestReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { HooksService } from '../services/hooks.service';
import type { HookType } from 'nocodb-sdk';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class HooksController {
constructor(private readonly hooksService: HooksService) {}
@Get('/api/v1/db/meta/tables/:tableId/hooks')
@Acl('hookList')
async hookList(@Param('tableId') tableId: string) {
return new PagedResponseImpl(await this.hooksService.hookList({ tableId }));
}
@Post('/api/v1/db/meta/tables/:tableId/hooks')
@HttpCode(200)
@Acl('hookCreate')
async hookCreate(
@Param('tableId') tableId: string,
@Body() body: HookReqType,
) {
const hook = await this.hooksService.hookCreate({
hook: body,
tableId,
});
return hook;
}
@Delete('/api/v1/db/meta/hooks/:hookId')
@Acl('hookDelete')
async hookDelete(@Param('hookId') hookId: string) {
return await this.hooksService.hookDelete({ hookId });
}
@Patch('/api/v1/db/meta/hooks/:hookId')
@Acl('hookUpdate')
async hookUpdate(@Param('hookId') hookId: string, @Body() body: HookReqType) {
return await this.hooksService.hookUpdate({ hookId, hook: body });
}
@Post('/api/v1/db/meta/tables/:tableId/hooks/test')
@HttpCode(200)
@Acl('hookTest')
async hookTest(@Body() body: HookTestReqType, @Request() req: any) {
try {
await this.hooksService.hookTest({
hookTest: {
...body,
payload: {
...body.payload,
user: (req as any)?.user,
},
},
tableId: req.params.tableId,
});
return { msg: 'The hook has been tested successfully' };
} catch (e) {
console.error(e);
throw e;
}
}
@Get(
'/api/v1/db/meta/tables/:tableId/hooks/samplePayload/:operation/:version',
)
@Acl('tableSampleData')
async tableSampleData(
@Param('tableId') tableId: string,
@Param('operation') operation: HookType['operation'],
@Param('version') version: HookType['version'],
) {
return await this.hooksService.tableSampleData({
tableId,
operation,
version,
});
}
@Get('/api/v1/db/meta/hooks/:hookId/logs')
@Acl('hookLogList')
async hookLogList(@Param('hookId') hookId: string, @Request() req: any) {
return new PagedResponseImpl(
await this.hooksService.hookLogList({
query: req.query,
hookId,
}),
{
...req.query,
count: await this.hooksService.hookLogCount({
hookId,
}),
},
);
}
}

222
packages/nocodb-nest/src/controllers/imports/helpers/EntityMap.ts

@ -0,0 +1,222 @@
import { Readable } from 'stream';
import sqlite3 from 'sqlite3';
class EntityMap {
initialized: boolean;
cols: string[];
db: any;
constructor(...args) {
this.initialized = false;
this.cols = args.map((arg) => processKey(arg));
this.db = new Promise((resolve, reject) => {
const db = new sqlite3.Database(':memory:');
const colStatement =
this.cols.length > 0
? this.cols.join(' TEXT, ') + ' TEXT'
: 'mappingPlaceholder TEXT';
db.run(`CREATE TABLE mapping (${colStatement})`, (err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(db);
});
});
}
async init() {
if (!this.initialized) {
this.db = await this.db;
this.initialized = true;
}
}
destroy() {
if (this.initialized && this.db) {
this.db.close();
}
}
async addRow(row) {
if (!this.initialized) {
throw 'Please initialize first!';
}
const cols = Object.keys(row).map((key) => processKey(key));
const colStatement = cols.map((key) => `'${key}'`).join(', ');
const questionMarks = cols.map(() => '?').join(', ');
const promises = [];
for (const col of cols.filter((col) => !this.cols.includes(col))) {
promises.push(
new Promise((resolve, reject) => {
this.db.run(`ALTER TABLE mapping ADD '${col}' TEXT;`, (err) => {
if (err) {
console.log(err);
reject(err);
}
this.cols.push(col);
resolve(true);
});
}),
);
}
await Promise.all(promises);
const values = Object.values(row).map((val) => {
if (typeof val === 'object') {
return `JSON::${JSON.stringify(val)}`;
}
return val;
});
return new Promise((resolve, reject) => {
this.db.run(
`INSERT INTO mapping (${colStatement}) VALUES (${questionMarks})`,
values,
(err) => {
if (err) {
console.log(err);
reject(err);
}
resolve(true);
},
);
});
}
getRow(col, val, res = []): Promise<Record<string, any>> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
col = processKey(col);
res = res.map((r) => processKey(r));
this.db.get(
`SELECT ${
res.length ? res.join(', ') : '*'
} FROM mapping WHERE ${col} = ?`,
[val],
(err, rs) => {
if (err) {
console.log(err);
reject(err);
}
if (rs) {
rs = processResponseRow(rs);
}
resolve(rs);
},
);
});
}
getCount(): Promise<number> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
this.db.get(`SELECT COUNT(*) as count FROM mapping`, (err, rs) => {
if (err) {
console.log(err);
reject(err);
}
resolve(rs.count);
});
});
}
getStream(res = []): DBStream {
if (!this.initialized) {
throw 'Please initialize first!';
}
res = res.map((r) => processKey(r));
return new DBStream(
this.db,
`SELECT ${res.length ? res.join(', ') : '*'} FROM mapping`,
);
}
getLimit(limit, offset, res = []): Promise<Record<string, any>[]> {
if (!this.initialized) {
throw 'Please initialize first!';
}
return new Promise((resolve, reject) => {
res = res.map((r) => processKey(r));
this.db.all(
`SELECT ${
res.length ? res.join(', ') : '*'
} FROM mapping LIMIT ${limit} OFFSET ${offset}`,
(err, rs) => {
if (err) {
console.log(err);
reject(err);
}
for (let row of rs) {
row = processResponseRow(row);
}
resolve(rs);
},
);
});
}
}
class DBStream extends Readable {
db: any;
stmt: any;
sql: any;
constructor(db, sql) {
super({ objectMode: true });
this.db = db;
this.sql = sql;
this.stmt = this.db.prepare(this.sql);
this.on('end', () => this.stmt.finalize());
}
_read() {
const stream = this;
this.stmt.get(function (err, result) {
if (err) {
stream.emit('error', err);
} else {
if (result) {
result = processResponseRow(result);
}
stream.push(result || null);
}
});
}
}
function processResponseRow(res: any) {
for (const key of Object.keys(res)) {
if (res[key] && res[key].startsWith('JSON::')) {
try {
res[key] = JSON.parse(res[key].replace('JSON::', ''));
} catch (e) {
console.log(e);
}
}
if (revertKey(key) !== key) {
res[revertKey(key)] = res[key];
delete res[key];
}
}
return res;
}
function processKey(key) {
return key.replace(/'/g, "''").replace(/[A-Z]/g, (match) => `_${match}`);
}
function revertKey(key) {
return key.replace(/''/g, "'").replace(/_[A-Z]/g, (match) => match[1]);
}
export default EntityMap;

6
packages/nocodb-nest/src/controllers/imports/helpers/NocoSyncDestAdapter.ts

@ -0,0 +1,6 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract destProjectWrite(): Promise<any>;
public abstract destSchemaWrite(): Promise<any>;
public abstract destDataWrite(): Promise<any>;
}

7
packages/nocodb-nest/src/controllers/imports/helpers/NocoSyncSourceAdapter.ts

@ -0,0 +1,7 @@
export abstract class NocoSyncSourceAdapter {
public abstract init(): Promise<void>;
public abstract srcSchemaGet(): Promise<any>;
public abstract srcDataLoad(): Promise<any>;
public abstract srcDataListen(): Promise<any>;
public abstract srcDataPoll(): Promise<any>;
}

242
packages/nocodb-nest/src/controllers/imports/helpers/fetchAT.ts

@ -0,0 +1,242 @@
import axios from 'axios';
const info: any = {
initialized: false,
};
async function initialize(shareId) {
info.cookie = '';
const url = `https://airtable.com/${shareId}`;
try {
const hreq = await axios
.get(url, {
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
},
// @ts-ignore
referrerPolicy: 'strict-origin-when-cross-origin',
body: null,
method: 'GET',
})
.then((response) => {
for (const ck of response.headers['set-cookie']) {
info.cookie += ck.split(';')[0] + '; ';
}
return response.data;
})
.catch(() => {
throw {
message:
'Invalid Shared Base ID :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
info.headers = JSON.parse(
hreq.match(/(?<=var headers =)(.*)(?=;)/g)[0].trim(),
);
info.link = unicodeToChar(hreq.match(/(?<=fetch\(")(.*)(?=")/g)[0].trim());
info.baseInfo = decodeURIComponent(info.link)
.match(/{(.*)}/g)[0]
.split('&')
.reduce((result, el) => {
try {
return Object.assign(
result,
JSON.parse(el.includes('=') ? el.split('=')[1] : el),
);
} catch (e) {
if (el.includes('=')) {
return Object.assign(result, {
[el.split('=')[0]]: el.split('=')[1],
});
}
}
}, {});
info.baseId = info.baseInfo.applicationId;
info.initialized = true;
} catch (e) {
console.log(e);
info.initialized = false;
if (e.message) {
throw e;
} else {
throw {
message:
'Error processing Shared Base :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
}
}
}
async function read() {
if (info.initialized) {
const resreq = await axios('https://airtable.com' + info.link, {
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
// @ts-ignore
referrerPolicy: 'no-referrer',
body: null,
method: 'GET',
})
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Reading :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
return {
schema: resreq.data,
baseId: info.baseId,
baseInfo: info.baseInfo,
};
} else {
throw {
message: 'Error Initializing :: please try again !!',
};
}
}
async function readView(viewId) {
if (info.initialized) {
const resreq = await axios(
`https://airtable.com/v0.3/view/${viewId}/readData?` +
`stringifiedObjectParams=${encodeURIComponent('{}')}&requestId=${
info.baseInfo.requestId
}&accessPolicy=${encodeURIComponent(
JSON.stringify({
allowedActions: info.baseInfo.allowedActions,
shareId: info.baseInfo.shareId,
applicationId: info.baseInfo.applicationId,
generationNumber: info.baseInfo.generationNumber,
expires: info.baseInfo.expires,
signature: info.baseInfo.signature,
}),
)}`,
{
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
// @ts-ignore
referrerPolicy: 'no-referrer',
body: null,
method: 'GET',
},
)
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Reading View :: Ensure www.airtable.com/<SharedBaseID> is accessible. Refer https://bit.ly/3x0OdXI for details',
};
});
return { view: resreq.data };
} else {
throw {
message: 'Error Initializing :: please try again !!',
};
}
}
async function readTemplate(templateId) {
if (!info.initialized) {
await initialize('shrO8aYf3ybwSdDKn');
}
const resreq = await axios(
`https://www.airtable.com/v0.3/exploreApplications/${templateId}`,
{
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua':
'" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'User-Agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
'x-time-zone': 'Europe/Berlin',
cookie: info.cookie,
...info.headers,
},
// @ts-ignore
referrer: 'https://www.airtable.com/',
referrerPolicy: 'same-origin',
body: null,
method: 'GET',
mode: 'cors',
credentials: 'include',
},
)
.then((response) => {
return response.data;
})
.catch(() => {
throw {
message:
'Error Fetching :: Ensure www.airtable.com/templates/featured/<TemplateID> is accessible.',
};
});
return { template: resreq };
}
function unicodeToChar(text) {
return text.replace(/\\u[\dA-F]{4}/gi, function (match) {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
});
}
export default {
initialize,
read,
readView,
readTemplate,
};

2480
packages/nocodb-nest/src/controllers/imports/helpers/job.ts

File diff suppressed because it is too large Load Diff

361
packages/nocodb-nest/src/controllers/imports/helpers/readAndProcessData.ts

@ -0,0 +1,361 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import EntityMap from './EntityMap';
import type { BulkDataAliasService } from '../../../services/bulk-data-alias.service';
import type { TablesService } from '../../../services/tables.service';
// @ts-ignore
import type { AirtableBase } from 'airtable/lib/airtable_base';
import type { TableType } from 'nocodb-sdk';
const BULK_DATA_BATCH_SIZE = 500;
const ASSOC_BULK_DATA_BATCH_SIZE = 1000;
const BULK_PARALLEL_PROCESS = 5;
interface AirtableImportContext {
bulkDataService: BulkDataAliasService;
tableService: TablesService;
}
async function readAllData({
table,
fields,
base,
logBasic = (_str) => {},
services,
}: {
table: { title?: string };
fields?;
base: AirtableBase;
logBasic?: (string) => void;
logDetailed?: (string) => void;
services: AirtableImportContext;
}): Promise<EntityMap> {
return new Promise((resolve, reject) => {
let data = null;
const selectParams: any = {
pageSize: 100,
};
if (fields) selectParams.fields = fields;
base(table.title)
.select(selectParams)
.eachPage(
async function page(records, fetchNextPage) {
if (!data) {
data = new EntityMap();
await data.init();
}
for await (const record of records) {
await data.addRow({ id: record.id, ...record.fields });
}
const tmpLength = await data.getCount();
logBasic(
`:: Reading '${table.title}' data :: ${Math.max(
1,
tmpLength - records.length,
)} - ${tmpLength}`,
);
// To fetch the next page of records, call `fetchNextPage`.
// If there are more records, `page` will get called again.
// If there are no more records, `done` will get called.
fetchNextPage();
},
async function done(err) {
if (err) {
console.error(err);
return reject(err);
}
resolve(data);
},
);
});
}
export async function importData({
projectName,
table,
base,
nocoBaseDataProcessing_v2,
sDB,
logDetailed = (_str) => {},
logBasic = (_str) => {},
services,
}: {
projectName: string;
table: { title?: string; id?: string };
fields?;
base: AirtableBase;
logBasic: (string) => void;
logDetailed: (string) => void;
nocoBaseDataProcessing_v2;
sDB;
services: AirtableImportContext;
}): Promise<EntityMap> {
try {
// @ts-ignore
const records = await readAllData({
table,
base,
logDetailed,
logBasic,
});
await new Promise(async (resolve) => {
const readable = records.getStream();
const allRecordsCount = await records.getCount();
const promises = [];
let tempData = [];
let importedCount = 0;
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(
new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: rid, ...fields } = record;
const r = await nocoBaseDataProcessing_v2(sDB, table, {
id: rid,
fields,
});
tempData.push(r);
if (tempData.length >= BULK_DATA_BATCH_SIZE) {
let insertArray = tempData.splice(0, tempData.length);
await services.bulkDataService.bulkDataInsert({
projectName,
tableName: table.title,
body: insertArray,
cookie: {},
});
logBasic(
`:: Importing '${
table.title
}' data :: ${importedCount} - ${Math.min(
importedCount + BULK_DATA_BATCH_SIZE,
allRecordsCount,
)}`,
);
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
}),
);
});
readable.on('end', async () => {
await Promise.all(promises);
if (tempData.length > 0) {
await services.bulkDataService.bulkDataInsert({
projectName,
tableName: table.title,
body: tempData,
cookie: {},
});
logBasic(
`:: Importing '${
table.title
}' data :: ${importedCount} - ${Math.min(
importedCount + BULK_DATA_BATCH_SIZE,
allRecordsCount,
)}`,
);
importedCount += tempData.length;
tempData = [];
}
resolve(true);
});
});
return records;
} catch (e) {
console.log(e);
return null;
}
}
export async function importLTARData({
table,
fields,
base,
projectName,
insertedAssocRef = {},
logDetailed = (_str) => {},
logBasic = (_str) => {},
records,
atNcAliasRef,
ncLinkMappingTable,
syncDB,
services,
}: {
projectName: string;
table: { title?: string; id?: string };
fields;
base: AirtableBase;
logDetailed: (string) => void;
logBasic: (string) => void;
insertedAssocRef: { [assocTableId: string]: boolean };
records?: EntityMap;
atNcAliasRef: {
[ncTableId: string]: {
[ncTitle: string]: string;
};
};
ncLinkMappingTable: Record<string, Record<string, any>>[];
syncDB;
services: AirtableImportContext;
}) {
const assocTableMetas: Array<{
modelMeta: { id?: string; title?: string };
colMeta: { title?: string };
curCol: { title?: string };
refCol: { title?: string };
}> = [];
const allData =
records ||
(await readAllData({
table,
fields,
base,
logDetailed,
logBasic,
services,
}));
const modelMeta: any =
await services.tableService.getTableWithAccessibleViews({
tableId: table.id,
user: syncDB.user,
});
for (const colMeta of modelMeta.columns) {
// skip columns which are not LTAR and Many to many
if (
colMeta.uidt !== UITypes.LinkToAnotherRecord ||
colMeta.colOptions.type !== RelationTypes.MANY_TO_MANY
) {
continue;
}
// skip if already inserted
if (colMeta.colOptions.fk_mm_model_id in insertedAssocRef) continue;
// self links: skip if the column under consideration is the add-on column NocoDB creates
if (ncLinkMappingTable.every((a) => a.nc.title !== colMeta.title)) continue;
// mark as inserted
insertedAssocRef[colMeta.colOptions.fk_mm_model_id] = true;
const assocModelMeta: TableType =
(await services.tableService.getTableWithAccessibleViews({
tableId: colMeta.colOptions.fk_mm_model_id,
user: syncDB.user,
})) as any;
// extract associative table and columns meta
assocTableMetas.push({
modelMeta: assocModelMeta,
colMeta,
curCol: assocModelMeta.columns.find(
(c) => c.id === colMeta.colOptions.fk_mm_child_column_id,
),
refCol: assocModelMeta.columns.find(
(c) => c.id === colMeta.colOptions.fk_mm_parent_column_id,
),
});
}
let nestedLinkCnt = 0;
// Iterate over all related M2M associative table
for await (const assocMeta of assocTableMetas) {
let assocTableData = [];
let importedCount = 0;
// extract insert data from records
await new Promise((resolve) => {
const promises = [];
const readable = allData.getStream();
let activeProcess = 0;
readable.on('data', async (record) => {
promises.push(
new Promise(async (resolve) => {
activeProcess++;
if (activeProcess >= BULK_PARALLEL_PROCESS) readable.pause();
const { id: _atId, ...rec } = record;
// todo: use actual alias instead of sanitized
assocTableData.push(
...(
rec?.[atNcAliasRef[table.id][assocMeta.colMeta.title]] || []
).map((id) => ({
[assocMeta.curCol.title]: record.id,
[assocMeta.refCol.title]: id,
})),
);
if (assocTableData.length >= ASSOC_BULK_DATA_BATCH_SIZE) {
let insertArray = assocTableData.splice(0, assocTableData.length);
logBasic(
`:: Importing '${
table.title
}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
insertArray.length,
)}`,
);
await services.bulkDataService.bulkDataInsert({
projectName,
tableName: assocMeta.modelMeta.title,
body: insertArray,
cookie: {},
});
importedCount += insertArray.length;
insertArray = [];
}
activeProcess--;
if (activeProcess < BULK_PARALLEL_PROCESS) readable.resume();
resolve(true);
}),
);
});
readable.on('end', async () => {
await Promise.all(promises);
if (assocTableData.length >= 0) {
logBasic(
`:: Importing '${
table.title
}' LTAR data :: ${importedCount} - ${Math.min(
importedCount + ASSOC_BULK_DATA_BATCH_SIZE,
assocTableData.length,
)}`,
);
await services.bulkDataService.bulkDataInsert({
projectName,
tableName: assocMeta.modelMeta.title,
body: assocTableData,
cookie: {},
});
importedCount += assocTableData.length;
assocTableData = [];
}
resolve(true);
});
});
nestedLinkCnt += importedCount;
}
return nestedLinkCnt;
}

31
packages/nocodb-nest/src/controllers/imports/helpers/syncMap.ts

@ -0,0 +1,31 @@
export const mapTbl = {};
// static mapping records between aTblId && ncId
export const addToMappingTbl = function addToMappingTbl(
aTblId,
ncId,
ncName,
parent?,
) {
mapTbl[aTblId] = {
ncId: ncId,
ncParent: parent,
// name added to assist in quick debug
ncName: ncName,
};
};
// get NcID from airtable ID
export const getNcIdFromAtId = function getNcIdFromAtId(aId) {
return mapTbl[aId]?.ncId;
};
// get nc Parent from airtable ID
export const getNcParentFromAtId = function getNcParentFromAtId(aId) {
return mapTbl[aId]?.ncParent;
};
// get nc-title from airtable ID
export const getNcNameFromAtId = function getNcNameFromAtId(aId) {
return mapTbl[aId]?.ncName;
};

21
packages/nocodb-nest/src/controllers/imports/import.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { ImportService } from '../../services/import.service';
import { ImportController } from './import.controller';
import type { TestingModule } from '@nestjs/testing';
describe('ImportController', () => {
let controller: ImportController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ImportController],
providers: [ImportService],
}).compile();
controller = module.get<ImportController>(ImportController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

148
packages/nocodb-nest/src/controllers/imports/import.controller.ts

@ -0,0 +1,148 @@
import { Controller, HttpCode, Post, Request, UseGuards } from '@nestjs/common';
import { forwardRef, Inject } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { GlobalGuard } from '../../guards/global/global.guard';
import { NcError } from '../../helpers/catchError';
import { ExtractProjectIdMiddleware } from '../../middlewares/extract-project-id/extract-project-id.middleware';
import { SyncSource } from '../../models';
import NocoJobs from '../../jobs/NocoJobs';
import { SocketService } from '../../services/socket.service';
import airtableSyncJob from './helpers/job';
import type { AirtableSyncConfig } from './helpers/job';
import type { Server } from 'socket.io';
const AIRTABLE_IMPORT_JOB = 'AIRTABLE_IMPORT_JOB';
const AIRTABLE_PROGRESS_JOB = 'AIRTABLE_PROGRESS_JOB';
enum SyncStatus {
PROGRESS = 'PROGRESS',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
const initJob = (sv: Server, jobs: { [p: string]: { last_message: any } }) => {
// add importer job handler and progress notification job handler
NocoJobs.jobsMgr.addJobWorker(AIRTABLE_IMPORT_JOB, airtableSyncJob);
NocoJobs.jobsMgr.addJobWorker(
AIRTABLE_PROGRESS_JOB,
({ payload, progress }) => {
sv.to(payload?.id).emit('progress', {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
});
if (payload?.id in jobs) {
jobs[payload?.id].last_message = {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
};
}
},
);
NocoJobs.jobsMgr.addProgressCbk(AIRTABLE_IMPORT_JOB, (payload, progress) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: progress?.msg,
level: progress?.level,
status: progress?.status,
},
});
});
NocoJobs.jobsMgr.addSuccessCbk(AIRTABLE_IMPORT_JOB, (payload) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: 'Complete!',
status: SyncStatus.COMPLETED,
},
});
delete jobs[payload?.id];
});
NocoJobs.jobsMgr.addFailureCbk(AIRTABLE_IMPORT_JOB, (payload, error: any) => {
NocoJobs.jobsMgr.add(AIRTABLE_PROGRESS_JOB, {
payload,
progress: {
msg: error?.message || 'Failed due to some internal error',
status: SyncStatus.FAILED,
},
});
delete jobs[payload?.id];
});
};
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class ImportController {
constructor(
private readonly socketService: SocketService,
@Inject(forwardRef(() => ModuleRef)) private readonly moduleRef: ModuleRef,
) {}
@Post('/api/v1/db/meta/import/airtable')
@HttpCode(200)
importAirtable(@Request() req) {
NocoJobs.jobsMgr.add(AIRTABLE_IMPORT_JOB, {
id: req.query.id,
...req.body,
});
return {};
}
@Post('/api/v1/db/meta/syncs/:syncId/trigger')
@HttpCode(200)
async triggerSync(@Request() req) {
if (req.params.syncId in this.socketService.jobs) {
NcError.badRequest('Sync already in progress');
}
const syncSource = await SyncSource.get(req.params.syncId);
const user = await syncSource.getUser();
// Treat default baseUrl as siteUrl from req object
let baseURL = (req as any).ncSiteUrl;
// if environment value avail use it
// or if it's docker construct using `PORT`
if (process.env.NC_DOCKER) {
baseURL = `http://localhost:${process.env.PORT || 8080}`;
}
setTimeout(() => {
NocoJobs.jobsMgr.add<AirtableSyncConfig>(AIRTABLE_IMPORT_JOB, {
id: req.params.syncId,
...(syncSource?.details || {}),
projectId: syncSource.project_id,
baseId: syncSource.base_id,
authToken: '',
baseURL,
user: user,
moduleRef: this.moduleRef,
});
}, 1000);
this.socketService.jobs[req.params.syncId] = {
last_message: {
msg: 'Sync started',
},
};
return {};
}
@Post('/api/v1/db/meta/syncs/:syncId/abort')
@HttpCode(200)
async abortImport(@Request() req) {
if (req.params.syncId in this.socketService.jobs) {
delete this.socketService.jobs[req.params.syncId];
}
return {};
}
async onModuleInit() {
initJob(this.socketService.io, this.socketService.jobs);
}
}

21
packages/nocodb-nest/src/controllers/kanbans.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { KanbansService } from '../services/kanbans.service';
import { KanbansController } from './kanbans.controller';
import type { TestingModule } from '@nestjs/testing';
describe('KanbansController', () => {
let controller: KanbansController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [KanbansController],
providers: [KanbansService],
}).compile();
controller = module.get<KanbansController>(KanbansController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

56
packages/nocodb-nest/src/controllers/kanbans.controller.ts

@ -0,0 +1,56 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { KanbansService } from '../services/kanbans.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class KanbansController {
constructor(private readonly kanbansService: KanbansService) {}
@Get('/api/v1/db/meta/kanbans/:kanbanViewId')
@Acl('kanbanViewGet')
async kanbanViewGet(@Param('kanbanViewId') kanbanViewId: string) {
return await this.kanbansService.kanbanViewGet({
kanbanViewId,
});
}
@Post('/api/v1/db/meta/tables/:tableId/kanbans')
@HttpCode(200)
@Acl('kanbanViewCreate')
async kanbanViewCreate(
@Param('tableId') tableId: string,
@Body() body: ViewCreateReqType,
) {
return await this.kanbansService.kanbanViewCreate({
tableId,
kanban: body,
});
}
@Patch('/api/v1/db/meta/kanbans/:kanbanViewId')
@Acl('kanbanViewUpdate')
async kanbanViewUpdate(
@Param('kanbanViewId') kanbanViewId: string,
@Body() body,
) {
return await this.kanbansService.kanbanViewUpdate({
kanbanViewId,
kanban: body,
});
}
}

21
packages/nocodb-nest/src/controllers/maps.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { MapsService } from '../services/maps.service';
import { MapsController } from './maps.controller';
import type { TestingModule } from '@nestjs/testing';
describe('MapsController', () => {
let controller: MapsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MapsController],
providers: [MapsService],
}).compile();
controller = module.get<MapsController>(MapsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

56
packages/nocodb-nest/src/controllers/maps.controller.ts

@ -0,0 +1,56 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { MapsService } from '../services/maps.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class MapsController {
constructor(private readonly mapsService: MapsService) {}
@Get('/api/v1/db/meta/maps/:mapViewId')
@Acl('mapViewGet')
async mapViewGet(@Param('mapViewId') mapViewId: string) {
return await this.mapsService.mapViewGet({ mapViewId });
}
@Post('/api/v1/db/meta/tables/:tableId/maps')
@HttpCode(200)
@Acl('mapViewCreate')
async mapViewCreate(
@Param('tableId') tableId: string,
@Body() body: ViewCreateReqType,
) {
const view = await this.mapsService.mapViewCreate({
tableId,
map: body,
});
return view;
}
@Patch('/api/v1/db/meta/maps/:mapViewId')
@Acl('mapViewUpdate')
async mapViewUpdate(
@Param('mapViewId') mapViewId: string,
@Body() body: MapUpdateReqType,
) {
return await this.mapsService.mapViewUpdate({
mapViewId: mapViewId,
map: body,
});
}
}

21
packages/nocodb-nest/src/controllers/meta-diffs.controller.spec.ts

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { MetaDiffsService } from '../services/meta-diffs.service';
import { MetaDiffsController } from './meta-diffs.controller';
import type { TestingModule } from '@nestjs/testing';
describe('MetaDiffsController', () => {
let controller: MetaDiffsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MetaDiffsController],
providers: [MetaDiffsService],
}).compile();
controller = module.get<MetaDiffsController>(MetaDiffsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

61
packages/nocodb-nest/src/controllers/meta-diffs.controller.ts

@ -0,0 +1,61 @@
import {
Controller,
Get,
HttpCode,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { MetaDiffsService } from '../services/meta-diffs.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class MetaDiffsController {
constructor(private readonly metaDiffsService: MetaDiffsService) {}
@Get('/api/v1/db/meta/projects/:projectId/meta-diff')
@Acl('metaDiff')
async metaDiff(@Param('projectId') projectId: string) {
return await this.metaDiffsService.metaDiff({ projectId });
}
@Get('/api/v1/db/meta/projects/:projectId/meta-diff/:baseId')
async baseMetaDiff(
@Param('projectId') projectId: string,
@Param('baseId') baseId: string,
) {
return await this.metaDiffsService.baseMetaDiff({
baseId,
projectId,
});
}
@Post('/api/v1/db/meta/projects/:projectId/meta-diff')
@HttpCode(200)
@Acl('metaDiffSync')
async metaDiffSync(@Param('projectId') projectId: string) {
await this.metaDiffsService.metaDiffSync({ projectId });
return { msg: 'The meta has been synchronized successfully' };
}
@Post('/api/v1/db/meta/projects/:projectId/meta-diff/:baseId')
@HttpCode(200)
@Acl('baseMetaDiffSync')
async baseMetaDiffSync(
@Param('projectId') projectId: string,
@Param('baseId') baseId: string,
) {
await this.metaDiffsService.baseMetaDiffSync({
projectId,
baseId,
});
return { msg: 'The base meta has been synchronized successfully' };
}
}

23
packages/nocodb-nest/src/controllers/model-visibilities.controller.spec.ts

@ -0,0 +1,23 @@
import { Test } from '@nestjs/testing';
import { ModelVisibilitiesService } from '../services/model-visibilities.service';
import { ModelVisibilitiesController } from './model-visibilities.controller';
import type { TestingModule } from '@nestjs/testing';
describe('ModelVisibilitiesController', () => {
let controller: ModelVisibilitiesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ModelVisibilitiesController],
providers: [ModelVisibilitiesService],
}).compile();
controller = module.get<ModelVisibilitiesController>(
ModelVisibilitiesController,
);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

52
packages/nocodb-nest/src/controllers/model-visibilities.controller.ts

@ -0,0 +1,52 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { ModelVisibilitiesService } from '../services/model-visibilities.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class ModelVisibilitiesController {
constructor(
private readonly modelVisibilitiesService: ModelVisibilitiesService,
) {}
@Post('/api/v1/db/meta/projects/:projectId/visibility-rules')
@HttpCode(200)
@Acl('modelVisibilitySet')
async xcVisibilityMetaSetAll(
@Param('projectId') projectId: string,
@Body() body: any,
) {
await this.modelVisibilitiesService.xcVisibilityMetaSetAll({
visibilityRule: body,
projectId,
});
return { msg: 'UI ACL has been created successfully' };
}
@Get('/api/v1/db/meta/projects/:projectId/visibility-rules')
@Acl('modelVisibilityList')
async modelVisibilityList(
@Param('projectId') projectId: string,
@Query('includeM2M') includeM2M: boolean | string,
) {
return await this.modelVisibilitiesService.xcVisibilityMetaGet({
projectId,
includeM2M: includeM2M === true || includeM2M === 'true',
});
}
}

19
packages/nocodb-nest/src/controllers/old-datas/old-datas.controller.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { OldDatasController } from './old-datas.controller';
import type { TestingModule } from '@nestjs/testing';
describe('OldDatasController', () => {
let controller: OldDatasController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [OldDatasController],
}).compile();
controller = module.get<OldDatasController>(OldDatasController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

138
packages/nocodb-nest/src/controllers/old-datas/old-datas.controller.ts

@ -0,0 +1,138 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../../guards/global/global.guard';
import {
Acl,
ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware';
import { OldDatasService } from './old-datas.service';
@Controller()
@UseGuards(ExtractProjectIdMiddleware, GlobalGuard)
export class OldDatasController {
constructor(private readonly oldDatasService: OldDatasService) {}
@Get('/nc/:projectId/api/v1/:tableName')
@Acl('dataList')
async dataList(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
) {
res.json(
await this.oldDatasService.dataList({
query: req.query,
projectId: projectId,
tableName: tableName,
}),
);
}
@Get('/nc/:projectId/api/v1/:tableName/count')
@Acl('dataCount')
async dataCount(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
) {
res.json(
await this.oldDatasService.dataCount({
query: req.query,
projectId: projectId,
tableName: tableName,
}),
);
}
@Post('/nc/:projectId/api/v1/:tableName')
@HttpCode(200)
@Acl('dataInsert')
async dataInsert(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
@Body() body: any,
) {
res.json(
await this.oldDatasService.dataInsert({
projectId: projectId,
tableName: tableName,
body: body,
cookie: req,
}),
);
}
@Get('/nc/:projectId/api/v1/:tableName/:rowId')
@Acl('dataRead')
async dataRead(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
@Param('rowId') rowId: string,
) {
res.json(
await this.oldDatasService.dataRead({
projectId: projectId,
tableName: tableName,
rowId: rowId,
query: req.query,
}),
);
}
@Patch('/nc/:projectId/api/v1/:tableName/:rowId')
@Acl('dataUpdate')
async dataUpdate(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
@Param('rowId') rowId: string,
) {
res.json(
await this.oldDatasService.dataUpdate({
projectId: projectId,
tableName: tableName,
body: req.body,
cookie: req,
rowId: rowId,
}),
);
}
@Delete('/nc/:projectId/api/v1/:tableName/:rowId')
@Acl('dataDelete')
async dataDelete(
@Request() req,
@Response() res,
@Param('projectId') projectId: string,
@Param('tableName') tableName: string,
@Param('rowId') rowId: string,
) {
res.json(
await this.oldDatasService.dataDelete({
projectId: projectId,
tableName: tableName,
cookie: req,
rowId: rowId,
}),
);
}
}

19
packages/nocodb-nest/src/controllers/old-datas/old-datas.service.spec.ts

@ -0,0 +1,19 @@
import { Test } from '@nestjs/testing';
import { OldDatasService } from './old-datas.service';
import type { TestingModule } from '@nestjs/testing';
describe('OldDatasService', () => {
let service: OldDatasService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [OldDatasService],
}).compile();
service = module.get<OldDatasService>(OldDatasService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

142
packages/nocodb-nest/src/controllers/old-datas/old-datas.service.ts

@ -0,0 +1,142 @@
import { Injectable } from '@nestjs/common';
import { nocoExecute } from 'nc-help';
import getAst from '../../helpers/getAst';
import { NcError } from '../../helpers/catchError';
import { Base, Model, Project, View } from '../../models';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import type { OldPathParams } from '../../modules/datas/helpers';
@Injectable()
export class OldDatasService {
async dataList(param: OldPathParams & { query: any }) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const { ast } = await getAst({
query: param.query,
model,
view,
});
const listArgs: any = { ...param.query };
try {
listArgs.filterArr = JSON.parse(listArgs.filterArrJson);
} catch (e) {}
try {
listArgs.sortArr = JSON.parse(listArgs.sortArrJson);
} catch (e) {}
return await nocoExecute(ast, await baseModel.list(listArgs), {}, listArgs);
}
async dataCount(param: OldPathParams & { query: any }) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const listArgs: any = { ...param.query };
try {
listArgs.filterArr = JSON.parse(listArgs.filterArrJson);
} catch (e) {}
return await baseModel.count(listArgs);
}
async dataInsert(param: OldPathParams & { body: unknown; cookie: any }) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
return await baseModel.insert(param.body, null, param.cookie);
}
async dataRead(param: OldPathParams & { query: any; rowId: string }) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view?.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
const { ast } = await getAst({
query: param.query,
model,
view,
});
return await nocoExecute(
ast,
await baseModel.readByPk(param.rowId),
{},
{},
);
}
async dataUpdate(
param: OldPathParams & { body: unknown; cookie: any; rowId: string },
) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
return await baseModel.updateByPk(
param.rowId,
param.body,
null,
param.cookie,
);
}
async dataDelete(param: OldPathParams & { rowId: string; cookie: any }) {
const { model, view } = await this.getViewAndModelFromRequest(param);
const base = await Base.get(model.base_id);
const baseModel = await Model.getBaseModelSQL({
id: model.id,
viewId: view.id,
dbDriver: await NcConnectionMgrv2.get(base),
});
return await baseModel.delByPk(param.rowId, null, param.cookie);
}
async getViewAndModelFromRequest(req) {
const project = await Project.getWithInfo(req.params.projectId);
const model = await Model.getByAliasOrId({
project_id: project.id,
aliasOrId: req.params.tableName,
});
const view =
req.params.viewName &&
(await View.getByTitleOrId({
titleOrId: req.params.viewName,
fk_model_id: model.id,
}));
if (!model) NcError.notFound('Table not found');
return { model, view };
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save