├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── docker.yml │ └── npm.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── Dockerfile ├── LICENSE ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── app ├── .env.example ├── .env.local ├── .env.production ├── .eslintrc.json ├── .gitignore ├── .nvrmc ├── .prettierrc.json ├── README.md ├── env.d.ts ├── index.html ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ ├── app │ │ │ ├── item-list.vue │ │ │ ├── retrieve-next.vue │ │ │ ├── table-actions.vue │ │ │ ├── table-filter.vue │ │ │ ├── table-list.vue │ │ │ └── table-paginate.vue │ │ └── common │ │ │ ├── SearchDropdown.vue │ │ │ └── bread-crumb.vue │ ├── constants │ │ ├── conditions.ts │ │ ├── dynamodb.ts │ │ ├── event.ts │ │ ├── routes.ts │ │ └── sort.ts │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── services │ │ ├── database.ts │ │ ├── item.ts │ │ └── table.ts │ ├── store │ │ ├── dynamodb.ts │ │ ├── index.ts │ │ ├── item.ts │ │ ├── table.ts │ │ └── ui.ts │ ├── utils │ │ ├── string.ts │ │ └── table.ts │ └── views │ │ ├── items │ │ ├── CreateItem.vue │ │ ├── EditItem.vue │ │ ├── HomeTable.vue │ │ └── codeMirrorConfig.ts │ │ └── table │ │ ├── CreateTable.vue │ │ ├── EditTable.vue │ │ ├── RestoreTables.vue │ │ ├── TableSchema.vue │ │ └── TableTTL.vue ├── tsconfig.config.json ├── tsconfig.json ├── vite.config.ts └── yarn.lock ├── docker-compose.build.yml ├── docker-compose.host.yml ├── docker-compose.yml ├── electron ├── .gitignore ├── assets │ ├── icon.png │ ├── splash.html │ └── splash.png ├── build ├── electron-builder.config.js ├── index.js ├── package-lock.json └── package.json ├── package.json ├── server ├── .babelrc ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .nvrmc ├── .prettierrc.json ├── README.md ├── package.json ├── public │ └── index.html ├── src │ ├── bin │ │ ├── cli.js │ │ └── server.js │ ├── config │ │ └── aws.js │ ├── constants │ │ ├── config.js │ │ ├── dynamodb.js │ │ └── event.js │ ├── controllers │ │ ├── database.controller.js │ │ ├── item.controller.js │ │ └── table.controller.js │ ├── errors │ │ └── handler.js │ ├── index.js │ ├── routes.js │ ├── routes │ │ ├── database.routes.js │ │ ├── item.routes.js │ │ └── table.routes.js │ ├── schemas │ │ ├── common │ │ │ ├── credentials.common.joi.js │ │ │ └── table.common.joi.js │ │ ├── database.joi.js │ │ ├── item.joi.js │ │ └── table.joi.js │ ├── services │ │ ├── database.service.js │ │ ├── item.service.js │ │ └── table.service.js │ ├── utils │ │ ├── dynamodb.js │ │ └── object.js │ └── validators │ │ ├── database.validators.js │ │ ├── item.validators.js │ │ └── table.validators.js └── yarn.lock └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Builds 2 | ./app/dist 3 | ./server/build 4 | 5 | # node_modules 6 | ./app/node_modules 7 | ./server/node_modules 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/app" 7 | schedule: 8 | interval: "daily" 9 | 10 | - package-ecosystem: "npm" 11 | directory: "/server" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: dynamodb-dashboard Builder 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set Node.js 22.x 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 22.x 22 | 23 | - name: Publish (--dry-run) 24 | run: make publish 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '33 5 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v3 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # This automatically builds a multi-arch image and pushes it to Docker Hub 2 | # whenever a new release is published. 3 | # 4 | # Required settings: 5 | # 6 | # The `REGISTRY_IMAGE` GitHub Actions environment variable should contain 7 | # the image name, e.g. `kritishdhaubanjar/dynamodb-dashboard`. 8 | # 9 | # The `DOCKER_USERNAME` GitHub Actions secret should contain your Docker 10 | # Hub username. 11 | # 12 | # The `DOCKER_PASSWORD` GitHub Actions secret should contain a Docker Hub 13 | # access token with Read and Write permissions. 14 | 15 | name: Publish Docker image 16 | on: 17 | release: 18 | types: [published] 19 | jobs: 20 | build: 21 | name: Push Docker image to Docker Hub 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | platform: 27 | - linux/amd64 28 | - linux/arm64 29 | permissions: 30 | packages: write 31 | contents: read 32 | steps: 33 | - name: Prepare 34 | run: | 35 | platform=${{ matrix.platform }} 36 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 37 | 38 | - name: Check out the repo 39 | uses: actions/checkout@v4 40 | 41 | - name: Log in to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKER_USERNAME }} 45 | password: ${{ secrets.DOCKER_PASSWORD }} 46 | 47 | - name: Set up QEMU 48 | uses: docker/setup-qemu-action@v3 49 | 50 | - name: Set up Docker Buildx 51 | uses: docker/setup-buildx-action@v3 52 | 53 | - name: Extract metadata (tags, labels) for Docker 54 | id: meta 55 | uses: docker/metadata-action@v5 56 | with: 57 | images: ${{ vars.REGISTRY_IMAGE }} 58 | 59 | - name: Build and push by digest 60 | id: build 61 | uses: docker/build-push-action@v5 62 | with: 63 | context: . 64 | platforms: ${{ matrix.platform }} 65 | labels: ${{ steps.meta.outputs.labels }} 66 | outputs: type=image,name=${{ vars.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true 67 | 68 | - name: Export digest 69 | run: | 70 | mkdir -p /tmp/digests 71 | digest="${{ steps.build.outputs.digest }}" 72 | touch "/tmp/digests/${digest#sha256:}" 73 | 74 | - name: Upload digest 75 | uses: actions/upload-artifact@v4 76 | env: 77 | PLATFORM_PAIR: ${{ env.PLATFORM_PAIR }} 78 | with: 79 | name: digests-${{ env.PLATFORM_PAIR }} 80 | path: /tmp/digests/* 81 | if-no-files-found: error 82 | retention-days: 1 83 | 84 | merge: 85 | runs-on: ubuntu-latest 86 | needs: 87 | - build 88 | steps: 89 | - name: Download digests 90 | uses: actions/download-artifact@v4 91 | with: 92 | path: /tmp/digests 93 | pattern: digests-* 94 | merge-multiple: true 95 | 96 | - name: Set up Docker Buildx 97 | uses: docker/setup-buildx-action@v3 98 | 99 | - name: Docker meta 100 | id: meta 101 | uses: docker/metadata-action@v5 102 | with: 103 | images: ${{ vars.REGISTRY_IMAGE }} 104 | 105 | - name: Login to Docker Hub 106 | uses: docker/login-action@v3 107 | with: 108 | username: ${{ secrets.DOCKER_USERNAME }} 109 | password: ${{ secrets.DOCKER_PASSWORD }} 110 | 111 | - name: Create manifest list and push 112 | working-directory: /tmp/digests 113 | run: | 114 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 115 | $(printf '${{ vars.REGISTRY_IMAGE }}@sha256:%s ' *) 116 | 117 | - name: Inspect image 118 | run: | 119 | docker buildx imagetools inspect ${{ vars.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} 120 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish npmjs package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '22.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | 19 | - name: Publish 20 | run: | 21 | make publish 22 | cd server 23 | npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.11.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PORT_ARG=4567 2 | ARG HOST_ARG=0.0.0.0 3 | ARG PREFIX_ARG=dynamodb 4 | 5 | # Stage I [server-builder] 6 | FROM node:22-alpine AS server-builder 7 | 8 | WORKDIR /usr/src/server 9 | COPY ["./server/package.json", "./server/yarn.lock", "./"] 10 | RUN yarn 11 | COPY ./server ./ 12 | ENV NODE_ENV=production 13 | RUN yarn build 14 | RUN rm -rf node_modules 15 | RUN yarn install --production --frozen-lockfile 16 | 17 | # Stage II [app-builder] 18 | FROM node:22-alpine AS app-builder 19 | 20 | ARG PREFIX_ARG 21 | WORKDIR /usr/src/app 22 | COPY ["./app/package.json", "./app/yarn.lock", "./"] 23 | RUN yarn 24 | COPY ./app ./ 25 | RUN sed -i "s/dynamodb/$PREFIX_ARG/g" .env.production 26 | ENV NODE_ENV=production 27 | RUN yarn build-only 28 | 29 | # Stage III [dynamodb-dashboard] 30 | FROM node:22-alpine 31 | 32 | ARG HOST_ARG 33 | ARG PORT_ARG 34 | ARG PREFIX_ARG 35 | 36 | ENV PORT=$PORT_ARG 37 | ENV HOST=$HOST_ARG 38 | ENV PREFIX=$PREFIX_ARG 39 | 40 | WORKDIR /usr/dynamodb-dashboard 41 | 42 | COPY --from=server-builder /usr/src/server/yarn.lock ./ 43 | COPY --from=server-builder /usr/src/server/package.json ./ 44 | COPY --from=server-builder /usr/src/server/build ./build 45 | COPY --from=server-builder /usr/src/server/node_modules ./node_modules 46 | COPY --from=app-builder /usr/src/app/dist ./build/public 47 | 48 | EXPOSE $PORT 49 | CMD ["sh", "-c", "node build/bin/cli.js start --prefix=$PREFIX --port=$PORT --host=$HOST -d"] 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kritish Dhaubanjar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NPM=npm 2 | YARN=yarn 3 | NODE=node 4 | VERSION=1.23.0 5 | APP=dynamodb-dashboard 6 | # hub.docker.com 7 | USERNAME=kritishdhaubanjar 8 | # registry 9 | VERDACCIO_REGISTRY=http://localhost:4873 10 | VERDACCIO_STORAGE=~/.local/share/verdaccio/storage/$(APP) 11 | 12 | all: clean install prepare 13 | export NODE_ENV=production 14 | cd ./app && $(YARN) build-only 15 | cd ./server && $(YARN) build 16 | cp -r ./app/dist ./server/build/public 17 | 18 | prepare: 19 | cd ./app && $(YARN) prettier 20 | cd ./server && $(YARN) prettier 21 | 22 | install: 23 | $(YARN) 24 | cd ./app && $(YARN) 25 | cd ./server && $(YARN) 26 | 27 | clean: 28 | rm -rf ./node_modules 29 | rm -rf ./app/dist 30 | rm -rf ./server/build 31 | rm -rf ./app/node_modules 32 | rm -rf ./server/node_modules 33 | 34 | start: all 35 | $(NODE) ./server/build/bin/cli.js start 36 | 37 | publish: all 38 | rm ./server/README.md 39 | cp ./README.md ./server/README.md 40 | cd ./server && $(NPM) publish --dry-run 41 | 42 | docker: 43 | docker build . -t $(USERNAME)/dynamodb-dashboard:$(VERSION) 44 | docker build . -t $(USERNAME)/dynamodb-dashboard:latest 45 | 46 | # development 47 | .server: 48 | cd ./server && $(YARN) dev 49 | 50 | .app: 51 | cd ./app && $(YARN) dev 52 | 53 | watch: install 54 | make -j 2 .server .app 55 | 56 | # registry (experimental) 57 | verdaccio: publish 58 | rm -rf $(VERDACCIO_STORAGE) 59 | $(NPM) --global uninstall $(APP) 60 | cd ./server && $(NPM) publish --registry $(VERDACCIO_REGISTRY) 61 | $(NPM) --global install $(APP) --registry $(VERDACCIO_REGISTRY) 62 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | Describe your changes in detail. 3 | 4 | ### Related Issue 5 | If suggesting a new feature or change, please discuss it in an issue first. _If fixing a bug, there should be an issue describing it with steps to reproduce. Please link to any related issues here: 6 | 7 | ### Motivation and Context 8 | Why is this change required? What problem does it solve? 9 | 10 | ### How Has This Been Tested? 11 | Please describe in detail how you tested your changes. Include details of your testing environment, and the tests you ran to see how your change affects other areas of the code, etc. 12 | 13 | ### Types of Changes 14 | - [ ] Bug fix (non-breaking change that fixes an issue) 15 | - [ ] New feature (non-breaking change that adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | 18 | ### Screenshots (if appropriate): 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![icons8-database-administrator-96](https://user-images.githubusercontent.com/25634165/204095372-d6d8362f-3a33-4ab7-8b97-330fe712c404.png) 2 | 3 | ## DynamoDB Dashboard 4 | 5 | A Web GUI Dashboard for local or remote [DynamoDB](https://aws.amazon.com/blogs/aws/dynamodb-local-for-desktop-development/), inspired from [dynamodb-admin](https://github.com/aaronshaf/dynamodb-admin). 6 | 7 | ![npm](https://img.shields.io/npm/v/dynamodb-dashboard?label=npm&style=flat-square) ![npm](https://img.shields.io/npm/dw/dynamodb-dashboard?style=flat-square) ![NPM](https://img.shields.io/npm/l/dynamodb-dashboard?style=flat-square) 8 | 9 | ![Docker Pulls](https://img.shields.io/docker/pulls/kritishdhaubanjar/dynamodb-dashboard?style=flat-square) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/kritishdhaubanjar/dynamodb-dashboard/latest?style=flat-square) 10 | 11 | [![dynamodb-dashboard Builder](https://img.shields.io/github/actions/workflow/status/kritish-dhaubanjar/dynamodb-dashboard/build.yml?branch=main&label=dynamodb-dashboard%20Builder&style=flat-square)](https://github.com/kritish-dhaubanjar/dynamodb-dashboard/actions/workflows/build.yml) 12 | 13 | #### Installation: 14 | Install application globally: 15 | ```shell 16 | npm install --global dynamodb-dashboard 17 | ``` 18 | 19 | Start `dynamodb-dashboard` instance: 20 | ```shell 21 | dynamodb-dashboard start 22 | ``` 23 | 24 | ##### Options 25 | - `-d, --debug` : show log output of running application (default: false) 26 | - `-p, --port ` : port to run app (default: 4567) 27 | - `-h, --host ` : host to run app (default: 127.0.0.1) 28 | 29 | #### Setting Environment variables 30 | Currently, following environment variables are supported, with default values: 31 | 32 | - `AWS_REGION` (default: us-west-2) 33 | - `AWS_ENDPOINT` (default: http://127.0.0.1:8000) 34 | - `AWS_ACCESS_KEY_ID` (default: fakeAccessKeyId) 35 | - `AWS_SECRET_ACCESS_KEY` (default: fakeSecretAccessKey) 36 | - `AWS_SESSION_TOKEN` (optional) 37 | 38 | To configure, set the AWS environment variables in the terminal session before launching `dynamodb-dashboard`, example in `.bashrc` file. 39 | 40 | ### Development Setup 41 | 1. [Setup Vue.js App](https://github.com/kritish-dhaubanjar/dynamodb-dashboard/tree/main/app) 42 | 2. [Setup Node Express Server](https://github.com/kritish-dhaubanjar/dynamodb-dashboard/tree/main/server) 43 | 44 | **OR** 45 | 46 | 1. ```shell 47 | git clone https://github.com/kritish-dhaubanjar/dynamodb-dashboard.git 48 | ``` 49 | 2. ```shell 50 | cd dynamodb-dashboard 51 | ``` 52 | 3. ```shell 53 | make watch 54 | ``` 55 | 56 | ## Docker 57 | 58 | ### Dockerfile 59 | 60 |
61 |

1. Build Docker image & run a container (from source & Dockerfile)

62 | 63 | **a. Clone Repository** 64 | 1. ```shell 65 | git clone https://github.com/kritish-dhaubanjar/dynamodb-dashboard.git 66 | ``` 67 | 2. ```shell 68 | cd dynamodb-dashboard 69 | ``` 70 | 71 | **b. Build Docker Image** 72 | ```shell 73 | docker build . -t dynamodb-dashboard:local 74 | ``` 75 | 76 | *Build Arguments:* 77 | - `PORT_ARG` (default: `4567`) 78 | - `HOST_ARG` (default: `0.0.0.0`) 79 | - `PREFIX_ARG` (default: `dynamodb`, prefix of route URIs) 80 | 81 | **c. Run Docker Container** 82 | ```shell 83 | docker run -p 8080:4567 dynamodb-dashboard:local 84 | ``` 85 | 86 | *Environment Variables:* 87 | - `AWS_REGION` (default: `us-west-2`) 88 | - `AWS_ENDPOINT` (default: `http://127.0.0.1:8000`) 89 | - `AWS_ACCESS_KEY_ID` (default: `fakeAccessKeyId`) 90 | - `AWS_SECRET_ACCESS_KEY` (default: `fakeSecretAccessKey`) 91 | - `AWS_SESSION_TOKEN` (optional) 92 | 93 | *NOTE: For dynamodb running in the host machine, use flag `--network=host` for running dynamodb-dashboard container.* 94 |
95 |
96 |

2. Run a container (from Docker Hub)

97 | 98 | 1. ```shell 99 | docker pull kritishdhaubanjar/dynamodb-dashboard:latest 100 | ``` 101 | 2. ```shell 102 | docker run -p 8080:4567 kritishdhaubanjar/dynamodb-dashboard:latest 103 | ``` 104 | 105 | *Environment Variables:* 106 | - `AWS_REGION` (default: `us-west-2`) 107 | - `AWS_ENDPOINT` (default: `http://127.0.0.1:8000`) 108 | - `AWS_ACCESS_KEY_ID` (default: `fakeAccessKeyId`) 109 | - `AWS_SECRET_ACCESS_KEY` (default: `fakeSecretAccessKey`) 110 | - `AWS_SESSION_TOKEN` (optional) 111 | 112 | *NOTE: For dynamodb running in the host machine, use flag `--network=host` for running dynamodb-dashboard container.* 113 |
114 | 115 | ### docker-compose 116 | 117 |
118 |

1. Build Docker image & run a container (from docker-compose.build.yml)

119 | 120 | *dynamoDB image (from docker hub) + dynamodb-dashboard image (built from source)* 121 | 122 | **a. Clone Repository** 123 | 1. ```shell 124 | git clone https://github.com/kritish-dhaubanjar/dynamodb-dashboard.git 125 | ``` 126 | 2. ```shell 127 | cd dynamodb-dashboard 128 | ``` 129 | 130 | **b. Build & Run Docker Image** 131 | ```shell 132 | docker-compose -f docker-compose.build.yml up 133 | ``` 134 |
135 |
136 |

2. Run a container (from docker-compose.yml)

137 | 138 | *dynamoDB image (from docker hub) + dynamodb-dashboard image (from docker hub)* 139 | 140 | ```shell 141 | docker-compose up 142 | ``` 143 |
144 |
145 |

3. Run a container (from docker-compose.host.yml) using host networking

146 | 147 | *dynamoDB (host network) + dynamodb-dashboard image (from docker hub)* 148 | 149 | ```shell 150 | docker-compose -f docker-compose.host.yml up 151 | ``` 152 |
153 | 154 | ## NGINX Config 155 | To configure Nginx to serve dynamodb-dashboard with (EventSource (Server-Sent Events or SSE) event stream), you need to ensure Nginx is correctly set up to handle long-lived HTTP connections and provide appropriate headers. Here's a basic example configuration: 156 | ```nginx 157 | server 158 | { 159 | listen 80; 160 | listen [::]:80; 161 | 162 | server_name _; 163 | 164 | proxy_read_timeout 1d; 165 | proxy_send_timeout 1d; 166 | proxy_connect_timeout 1d; 167 | 168 | location / 169 | { 170 | proxy_pass http://localhost:4567; 171 | } 172 | 173 | location /dynamodb/api/database/stream/ 174 | { 175 | proxy_buffering off; 176 | proxy_cache off; 177 | chunked_transfer_encoding off; 178 | 179 | add_header Content-Type text/event-stream; 180 | add_header Cache-Control no-cache; 181 | add_header Connection keep-alive; 182 | 183 | proxy_pass http://localhost:4567; 184 | } 185 | } 186 | ``` 187 | 188 | ### Preview: 189 | ![dynamodb-dashboard](https://user-images.githubusercontent.com/25634165/213922274-d70cde00-4d70-47ac-ab84-68b6f0933d58.png) 190 | 191 | ![image](https://user-images.githubusercontent.com/25634165/215118400-fe18ea87-5562-4e7d-be7b-ccf61a3fbe99.png) 192 | 193 | ### Demo: 194 | https://user-images.githubusercontent.com/25634165/192109458-a621bc06-788d-4d54-9dc2-9064380ee837.mp4 195 | 196 | [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-orange?style=for-the-badge&logo=buy-me-a-coffee)](https://www.buymeacoffee.com/kritishdhaubanjar) 197 | 198 | ## Contributors ✨ 199 | 200 | Thanks goes to these wonderful people: 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 |

Bimochan Shrestha

📖

Bipin Manandhar

📖

Nihal Maskey

📖

Biplap Bhattarai

📖

Jim Dabell

📖

yyyoichi

📖
212 | 213 | # License 214 | Distributed under the MIT License. See `LICENSE` for more information. 215 | -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE_URL=/dynamodb/ 2 | VITE_API_BASE_URL=http://localhost:8080/dynamodb/api 3 | -------------------------------------------------------------------------------- /app/.env.local: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE_URL=/dynamodb/ 2 | VITE_API_BASE_URL=http://localhost:4567/dynamodb/api 3 | -------------------------------------------------------------------------------- /app/.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE_URL=/dynamodb/ 2 | VITE_API_BASE_URL=/dynamodb/api 3 | -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:vue/vue3-essential", "prettier"], 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "parser": "@typescript-eslint/parser", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "vue"], 13 | "rules": { 14 | "linebreak-style": ["error", "unix"], 15 | "semi": ["error", "always"], 16 | "vue/script-indent": [ 17 | "error", 18 | 2, 19 | { 20 | "baseIndent": 1, 21 | "switchCase": 1 22 | } 23 | ], 24 | "@typescript-eslint/no-explicit-any": "warn", 25 | "@typescript-eslint/no-unused-vars": "warn" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /app/.nvrmc: -------------------------------------------------------------------------------- 1 | ../.nvmrc -------------------------------------------------------------------------------- /app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "vueIndentScriptAndStyle": true, 5 | "htmlWhitespaceSensitivity": "ignore", 6 | "printWidth": 120, 7 | "tabWidth": 2, 8 | "singleAttributePerLine": true 9 | } 10 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # dynamodb-dashboard-app 2 | ## Recommended IDE Setup 3 | 4 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 5 | 6 | ## Customize configuration 7 | 8 | See [Vite Configuration Reference](https://vitejs.dev/config/). 9 | 10 | ## Project Setup 11 | 12 | ```sh 13 | yarn 14 | ``` 15 | 16 | ###### Compile and Hot-Reload for Development 17 | 18 | ```sh 19 | yarn dev 20 | ``` 21 | 22 | ###### Type-Check, Compile and Minify for Production 23 | 24 | ```sh 25 | yarn build 26 | ``` 27 | 28 | ###### Ignore Type-Check, Compile and Minify for Production 29 | 30 | ```sh 31 | yarn build-only 32 | ``` 33 | 34 | ###### Lint with [ESLint](https://eslint.org/) 35 | 36 | ```sh 37 | yarn lint 38 | ``` 39 | 40 | #### Environment variables 41 | For local development with separate server instance: 42 | ```shell 43 | VITE_APP_BASE_URL=/dynamodb 44 | VITE_API_BASE_URL=http://localhost:4567/dynamodb/api 45 | ``` 46 | 47 | For build to bundle with server instance: 48 | ```shell 49 | VITE_APP_BASE_URL=/dynamodb 50 | VITE_API_BASE_URL=/dynamodb/api 51 | ``` 52 | 53 | **NOTE**: To bundle SPA Vue.js app, build `app`, and copy `app/dist` to `server/build/public`. 54 | -------------------------------------------------------------------------------- /app/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DynamoDB Management Console 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-dashboard-app", 3 | "type": "module", 4 | "license": "MIT", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check build-only", 9 | "preview": "vite preview --port 4173", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit", 12 | "prettify": "prettier --write 'src/**/*.{js,ts,vue,css}'", 13 | "prettier": "prettier --list-different 'src/**/*.{js,ts,vue,css}'", 14 | "lint": "eslint src/**/*.{ts,vue}", 15 | "lint:fix": "eslint src/**/*.{ts,vue} --fix" 16 | }, 17 | "dependencies": { 18 | "@codemirror/commands": "^6.8.1", 19 | "@codemirror/lang-javascript": "^6.2.4", 20 | "@codemirror/lang-json": "^6.0.1", 21 | "@codemirror/lint": "^6.8.5", 22 | "@codemirror/theme-one-dark": "^6.1.2", 23 | "@popperjs/core": "^2.11.8", 24 | "axios": "^1.9.0", 25 | "bootstrap": "5.3.6", 26 | "bootstrap-icons": "^1.13.1", 27 | "codemirror": "^6.0.1", 28 | "darkreader": "^4.9.105", 29 | "lodash": "^4.17.21", 30 | "vue": "^3.5.16", 31 | "vue-router": "^4.5.1" 32 | }, 33 | "devDependencies": { 34 | "@rushstack/eslint-patch": "^1.11.0", 35 | "@types/bootstrap": "^5.2.10", 36 | "@types/lodash": "^4.17.17", 37 | "@tsconfig/node22": "^22.0.2", 38 | "@types/node": "24", 39 | "@typescript-eslint/eslint-plugin": "^7.0.0", 40 | "@typescript-eslint/parser": "^6.21.0", 41 | "@vitejs/plugin-vue": "^5.2.4", 42 | "@vitejs/plugin-vue-jsx": "^4.2.0", 43 | "@vue/eslint-config-prettier": "^10.2.0", 44 | "@vue/eslint-config-typescript": "^14.5.0", 45 | "@vue/tsconfig": "^0.7.0", 46 | "eslint": "^8.57.0", 47 | "eslint-config-prettier": "^10.1.5", 48 | "eslint-plugin-vue": "^9.33.0", 49 | "module-alias": "^2.2.3", 50 | "npm-run-all": "^4.1.5", 51 | "prettier": "^3.5.3", 52 | "sass": "^1.89.2", 53 | "typescript": "~5.8.3", 54 | "vite": "^6.3.5", 55 | "vue-tsc": "2.2.10" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kritish-dhaubanjar/dynamodb-dashboard/1e7fdc7c651001d9bbecc75524a6e303b7dc5b8d/app/public/favicon.ico -------------------------------------------------------------------------------- /app/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /app/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&display=swap"); 3 | 4 | * { 5 | font-size: 14px; 6 | font-family: "Roboto", sans-serif; 7 | } 8 | 9 | input, 10 | button, 11 | select, 12 | a { 13 | box-shadow: none !important; 14 | } 15 | 16 | .modal-backdrop.show { 17 | display: none; 18 | } 19 | 20 | .modal { 21 | background-color: rgba(0, 0, 0, 0.5); 22 | } 23 | 24 | .min-width-min-content { 25 | min-width: min-content; 26 | } 27 | 28 | .vh-60 { 29 | height: 60vh !important; 30 | } 31 | 32 | .vw-30 { 33 | width: 30vw !important; 34 | } 35 | 36 | .bg-codemirror { 37 | background-color: #282c34; 38 | } 39 | -------------------------------------------------------------------------------- /app/src/components/app/retrieve-next.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 51 | -------------------------------------------------------------------------------- /app/src/components/app/table-actions.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 238 | 239 | 264 | -------------------------------------------------------------------------------- /app/src/components/app/table-list.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 85 | 86 | 97 | -------------------------------------------------------------------------------- /app/src/components/app/table-paginate.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 78 | -------------------------------------------------------------------------------- /app/src/components/common/SearchDropdown.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 115 | 116 | 144 | -------------------------------------------------------------------------------- /app/src/components/common/bread-crumb.vue: -------------------------------------------------------------------------------- 1 | 209 | 210 | 289 | -------------------------------------------------------------------------------- /app/src/constants/conditions.ts: -------------------------------------------------------------------------------- 1 | const FILTER_CONDITIONS_BY_TYPE = { 2 | S: [ 3 | { label: "=", value: "=" }, 4 | { label: "<>", value: "<>" }, 5 | { label: "<=", value: "<=" }, 6 | { label: "<", value: "<" }, 7 | { label: ">=", value: ">=" }, 8 | { label: ">", value: ">" }, 9 | { label: "Between", value: "between" }, 10 | { label: "Exists", value: "attribute_exists" }, 11 | { label: "Not exists", value: "attribute_not_exists" }, 12 | { label: "Contains", value: "contains" }, 13 | { label: "Not contains", value: "not contains" }, 14 | { label: "Begins with", value: "begins_with" }, 15 | ], 16 | N: [ 17 | { label: "=", value: "=" }, 18 | { label: "<>", value: "<>" }, 19 | { label: "<=", value: "<=" }, 20 | { label: "<", value: "<" }, 21 | { label: ">=", value: ">=" }, 22 | { label: ">", value: ">" }, 23 | { label: "Between", value: "between" }, 24 | { label: "Exists", value: "attribute_exists" }, 25 | { label: "Not exists", value: "attribute_not_exists" }, 26 | ], 27 | B: [ 28 | { label: "=", value: "=" }, 29 | { label: "<>", value: "<>" }, 30 | { label: "<=", value: "<=" }, 31 | { label: "<", value: "<" }, 32 | { label: ">=", value: ">=" }, 33 | { label: ">", value: ">" }, 34 | { label: "Between", value: "between" }, 35 | { label: "Exists", value: "attribute_exists" }, 36 | { label: "Not exists", value: "attribute_not_exists" }, 37 | { label: "Contains", value: "contains" }, 38 | { label: "Not contains", value: "not contains" }, 39 | { label: "Begins with", value: "begins_with" }, 40 | ], 41 | BOOL: [ 42 | { label: "=", value: "=" }, 43 | { label: "<>", value: "<>" }, 44 | { label: "Exists", value: "attribute_exists" }, 45 | { label: "Not exists", value: "attribute_not_exists" }, 46 | ], 47 | NULL: [ 48 | { label: "Exists", value: "attribute_exists" }, 49 | { label: "Not exists", value: "attribute_not_exists" }, 50 | ], 51 | }; 52 | 53 | export default FILTER_CONDITIONS_BY_TYPE; 54 | -------------------------------------------------------------------------------- /app/src/constants/dynamodb.ts: -------------------------------------------------------------------------------- 1 | const DYNAMODB = { 2 | OPERATIONS: { 3 | SCAN: "SCAN", 4 | QUERY: "QUERY", 5 | }, 6 | }; 7 | 8 | export const TTL = { 9 | STATUS: { 10 | DISABLED: "DISABLED", 11 | ENABLED: "ENABLED", 12 | }, 13 | }; 14 | 15 | export const AWS_REGIONS = [ 16 | "us-east-2", 17 | "us-east-1", 18 | "us-west-1", 19 | "us-west-2", 20 | "af-south-1", 21 | "ap-east-1", 22 | "ap-south-2", 23 | "ap-southeast-3", 24 | "ap-southeast-4", 25 | "ap-south-1", 26 | "ap-northeast-3", 27 | "ap-northeast-2", 28 | "ap-southeast-1", 29 | "ap-southeast-2", 30 | "ap-northeast-1", 31 | "ca-central-1", 32 | "eu-central-1", 33 | "eu-west-1", 34 | "eu-west-2", 35 | "eu-south-1", 36 | "eu-west-3", 37 | "eu-south-2", 38 | "eu-north-1", 39 | "eu-central-2", 40 | "il-central-1", 41 | "me-south-1", 42 | "me-central-1", 43 | "sa-east-1", 44 | "us-gov-east-1", 45 | "us-gov-west-1", 46 | ]; 47 | 48 | export const AWS_DYNAMODB_ENDPOINTS = [ 49 | "https://dynamodb.us-east-2.amazonaws.com", 50 | "https://dynamodb.us-east-1.amazonaws.com", 51 | "https://dynamodb.us-west-1.amazonaws.com", 52 | "https://dynamodb.us-west-2.amazonaws.com", 53 | "https://dynamodb.af-south-1.amazonaws.com", 54 | "https://dynamodb.ap-east-1.amazonaws.com", 55 | "https://dynamodb.ap-south-2.amazonaws.com", 56 | "https://dynamodb.ap-southeast-3.amazonaws.com", 57 | "https://dynamodb.ap-southeast-4.amazonaws.com", 58 | "https://dynamodb.ap-south-1.amazonaws.com", 59 | "https://dynamodb.ap-northeast-3.amazonaws.com", 60 | "https://dynamodb.ap-northeast-2.amazonaws.com", 61 | "https://dynamodb.ap-southeast-1.amazonaws.com", 62 | "https://dynamodb.ap-southeast-2.amazonaws.com", 63 | "https://dynamodb.ap-northeast-1.amazonaws.com", 64 | "https://dynamodb.ca-central-1.amazonaws.com", 65 | "https://dynamodb.eu-central-1.amazonaws.com", 66 | "https://dynamodb.eu-west-1.amazonaws.com", 67 | "https://dynamodb.eu-west-2.amazonaws.com", 68 | "https://dynamodb.eu-south-1.amazonaws.com", 69 | "https://dynamodb.eu-west-3.amazonaws.com", 70 | "https://dynamodb.eu-south-2.amazonaws.com", 71 | "https://dynamodb.eu-north-1.amazonaws.com", 72 | "https://dynamodb.eu-central-2.amazonaws.com", 73 | "https://dynamodb.il-central-1.amazonaws.com", 74 | "https://dynamodb.me-south-1.amazonaws.com", 75 | "https://dynamodb.me-central-1.amazonaws.com", 76 | "https://dynamodb.sa-east-1.amazonaws.com", 77 | "https://dynamodb.us-gov-east-1.amazonaws.com", 78 | "https://dynamodb.us-gov-west-1.amazonaws.com", 79 | ]; 80 | 81 | export default DYNAMODB; 82 | -------------------------------------------------------------------------------- /app/src/constants/event.ts: -------------------------------------------------------------------------------- 1 | export const EVENTS = { 2 | BEGIN: "BEGIN", 3 | SUCCESS: "SUCCESS", 4 | FAILURE: "FAILURE", 5 | END: "END", 6 | CLOSE: "CLOSE", 7 | ACK: "ACK", 8 | PROGRESS: "PROGRESS", 9 | }; 10 | -------------------------------------------------------------------------------- /app/src/constants/routes.ts: -------------------------------------------------------------------------------- 1 | const routes = { 2 | TABLE: { 3 | ALL: "/tables", 4 | CREATE: "/tables", 5 | GET: "/tables/:tableName", 6 | DELETE: "/tables/:tableName", 7 | UPDATE: "/tables/:tableName", 8 | TRUNCATE: "/tables/:tableName/truncate", 9 | DESCRIBE: "/tables/:tableName/describe", 10 | TTL: "/tables/:tableName/time-to-live", 11 | }, 12 | DATABASE: { 13 | ALL: "/database/tables", 14 | RESTORE: "/database/restore/:uid", 15 | STREAM: "/database/stream/:uid", 16 | CONNECT: "/database/connect", 17 | DISCONNECT: "/database/disconnect", 18 | ABORT: "/database/restore/abort", 19 | }, 20 | ITEM: { 21 | CREATE: "/tables/:tableName/items", 22 | GET: "/tables/:tableName/items/get", 23 | UPDATE: "/tables/:tableName/items", 24 | SCAN: "/tables/:tableName/items/scan", 25 | QUERY: "/tables/:tableName/items/query", 26 | COUNT: "/tables/:tableName/items/count", 27 | TRUNCATE: "/tables/:tableName/items/truncate", 28 | DELETE: "/tables/:tableName/items/delete", 29 | }, 30 | }; 31 | 32 | export default routes; 33 | -------------------------------------------------------------------------------- /app/src/constants/sort.ts: -------------------------------------------------------------------------------- 1 | export const SORT_ORDER = { 2 | NONE: null, 3 | ASC: "ASC", 4 | DESC: "DESC", 5 | }; 6 | 7 | export const SORTS = [SORT_ORDER.NONE, SORT_ORDER.ASC, SORT_ORDER.DESC]; 8 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { createApp } from "vue"; 3 | 4 | import store from "@/store"; 5 | 6 | import App from "@/App.vue"; 7 | import router from "@/router"; 8 | 9 | import "bootstrap"; 10 | import "@/assets/main.css"; 11 | import "bootstrap-icons/font/bootstrap-icons.css"; 12 | import "../node_modules/bootstrap/scss/bootstrap.scss"; 13 | 14 | axios.interceptors.request.use((config) => { 15 | store.ui.setters.setIsLoading(true); 16 | return config; 17 | }); 18 | 19 | axios.interceptors.response.use( 20 | (response) => { 21 | store.ui.setters.setIsLoading(false); 22 | return response; 23 | }, 24 | (error) => { 25 | store.ui.setters.setIsLoading(false); 26 | return Promise.reject(error); 27 | }, 28 | ); 29 | 30 | axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL; 31 | 32 | const app = createApp(App); 33 | app.use(router); 34 | app.mount("#app"); 35 | -------------------------------------------------------------------------------- /app/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import EditItem from "@/views/items/EditItem.vue"; 2 | import HomeTable from "@/views/items/HomeTable.vue"; 3 | import CreateItem from "@/views/items/CreateItem.vue"; 4 | 5 | import EditTable from "@/views/table/EditTable.vue"; 6 | import TableSchema from "@/views/table/TableSchema.vue"; 7 | import CreateTable from "@/views/table/CreateTable.vue"; 8 | import RestoreTables from "@/views/table/RestoreTables.vue"; 9 | import TableTTL from "@/views/table/TableTTL.vue"; 10 | 11 | import { createRouter, createWebHistory } from "vue-router"; 12 | 13 | const router = createRouter({ 14 | history: createWebHistory(import.meta.env.BASE_URL), 15 | routes: [ 16 | { 17 | path: "/", 18 | name: "home", 19 | component: HomeTable, 20 | meta: { 21 | name: "Item", 22 | }, 23 | }, 24 | { 25 | path: "/:tableName/edit-item", 26 | name: "edit-item", 27 | component: EditItem, 28 | meta: { 29 | name: "Item", 30 | }, 31 | }, 32 | { 33 | path: "/:tableName/create-item", 34 | name: "create-item", 35 | component: CreateItem, 36 | meta: { 37 | name: "Item", 38 | }, 39 | }, 40 | { 41 | path: "/table/create-table", 42 | name: "create-table", 43 | component: CreateTable, 44 | meta: { 45 | name: "Tables", 46 | }, 47 | }, 48 | { 49 | path: "/table/:tableName/edit-table", 50 | name: "edit-table", 51 | component: EditTable, 52 | meta: { 53 | name: "Tables", 54 | }, 55 | }, 56 | { 57 | path: "/table/:tableName/table-schema", 58 | name: "table-schema", 59 | component: TableSchema, 60 | meta: { 61 | name: "Tables", 62 | }, 63 | }, 64 | { 65 | path: "/table/:tableName/ttl", 66 | name: "table-ttl", 67 | component: TableTTL, 68 | meta: { 69 | name: "Tables", 70 | }, 71 | }, 72 | { 73 | path: "/table/restore-tables", 74 | name: "restore-tables", 75 | component: RestoreTables, 76 | meta: { 77 | name: "Tables", 78 | }, 79 | }, 80 | { 81 | path: "/:catchAll(.*)", 82 | component: HomeTable, 83 | meta: { 84 | name: "Items", 85 | }, 86 | }, 87 | /*{ 88 | path: "/about", 89 | name: "about", 90 | // route level code-splitting 91 | // this generates a separate chunk (About.[hash].js) for this route 92 | // which is lazy-loaded when the route is visited. 93 | component: () => import("../views/AboutView.vue"), 94 | },*/ 95 | ], 96 | }); 97 | 98 | export default router; 99 | -------------------------------------------------------------------------------- /app/src/services/database.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import ROUTES from "../constants/routes"; 3 | 4 | export async function connect(body: object) { 5 | const { data } = await axios.put(ROUTES.DATABASE.CONNECT, body); 6 | 7 | return data; 8 | } 9 | 10 | export async function disconnect() { 11 | const { data } = await axios.put(ROUTES.DATABASE.DISCONNECT, {}); 12 | 13 | return data; 14 | } 15 | -------------------------------------------------------------------------------- /app/src/services/item.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { isNil } from "lodash"; 3 | import ROUTES from "../constants/routes"; 4 | import { interpolate } from "../utils/string"; 5 | 6 | export async function queryItems(tableName: string, params: any) { 7 | const url = interpolate(ROUTES.ITEM.QUERY, { tableName }); 8 | 9 | const filteredParams: any = {}; 10 | 11 | Object.keys(params).forEach((key) => { 12 | if (!isNil(params[key])) { 13 | filteredParams[key] = params[key]; 14 | } 15 | }); 16 | 17 | const { data } = await axios.post(url, filteredParams); 18 | 19 | return data; 20 | } 21 | 22 | export async function scanItems(tableName: string, params: any) { 23 | const url = interpolate(ROUTES.ITEM.SCAN, { tableName }); 24 | 25 | const filteredParams: any = {}; 26 | 27 | Object.keys(params).forEach((key) => { 28 | if (!isNil(params[key])) { 29 | filteredParams[key] = params[key]; 30 | } 31 | }); 32 | 33 | const { data } = await axios.post(url, filteredParams); 34 | 35 | return data; 36 | } 37 | 38 | export async function countItems(tableName: string, params: any) { 39 | const url = interpolate(ROUTES.ITEM.COUNT, { tableName }); 40 | 41 | const filteredParams: any = {}; 42 | 43 | Object.keys(params).forEach((key) => { 44 | if (!isNil(params[key])) { 45 | filteredParams[key] = params[key]; 46 | } 47 | }); 48 | 49 | delete filteredParams.Limit; 50 | delete filteredParams.ExclusiveStartKey; 51 | 52 | const { data } = await axios.post(url, filteredParams); 53 | 54 | return data; 55 | } 56 | 57 | export async function truncateItems(tableName: string, params: any) { 58 | const url = interpolate(ROUTES.ITEM.TRUNCATE, { tableName }); 59 | 60 | const filteredParams: any = {}; 61 | 62 | Object.keys(params).forEach((key) => { 63 | if (!isNil(params[key])) { 64 | filteredParams[key] = params[key]; 65 | } 66 | }); 67 | 68 | delete filteredParams.Limit; 69 | delete filteredParams.ExclusiveStartKey; 70 | 71 | const { data } = await axios.put(url, filteredParams); 72 | 73 | return data; 74 | } 75 | 76 | export async function updateItem(tableName: string, original: any, body: any, KeySchema: []) { 77 | const url = interpolate(ROUTES.ITEM.UPDATE, { tableName }); 78 | 79 | const ref: any = {}; 80 | const newRef: any = {}; 81 | 82 | KeySchema.forEach(({ AttributeName }) => { 83 | ref[AttributeName] = original[AttributeName]; 84 | newRef[AttributeName] = body[AttributeName]; 85 | }); 86 | 87 | const { data } = await axios.put(url, { 88 | ref, 89 | body, 90 | }); 91 | 92 | return { data, ref: newRef, body }; 93 | } 94 | 95 | export async function getItem(tableName: string, params: any, AttributeDefinitions: []) { 96 | const url = interpolate(ROUTES.ITEM.GET, { tableName }); 97 | 98 | const formattedParams: any = {}; 99 | 100 | Object.keys(params).forEach((key) => { 101 | const { AttributeType } = AttributeDefinitions.find(({ AttributeName }) => AttributeName === key) ?? { 102 | AttributeType: "S", 103 | }; 104 | 105 | switch (AttributeType) { 106 | case "N": 107 | formattedParams[key] = parseInt(params[key]); 108 | break; 109 | default: 110 | formattedParams[key] = params[key]; 111 | break; 112 | } 113 | }); 114 | 115 | const { data } = await axios.post(url, formattedParams); 116 | 117 | return data; 118 | } 119 | 120 | export async function destroyItems(tableName: string, keys: []) { 121 | const url = interpolate(ROUTES.ITEM.DELETE, { tableName }); 122 | 123 | const params: any[] = []; 124 | 125 | keys.forEach((key) => { 126 | const param = { 127 | DeleteRequest: { 128 | Key: key, 129 | }, 130 | }; 131 | params.push(param); 132 | }); 133 | 134 | const { data } = await axios.post(url, params); 135 | 136 | return data; 137 | } 138 | 139 | export async function createItem(tableName: string, body: any) { 140 | const url = interpolate(ROUTES.ITEM.CREATE, { tableName }); 141 | 142 | const { data } = await axios.post(url, body); 143 | 144 | return { data, body }; 145 | } 146 | -------------------------------------------------------------------------------- /app/src/services/table.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import ROUTES from "../constants/routes"; 3 | import { interpolate } from "../utils/string"; 4 | 5 | export async function getTables() { 6 | const { data } = await axios.get(ROUTES.TABLE.ALL); 7 | return data; 8 | } 9 | 10 | export async function getTable(tableName: string) { 11 | const url = interpolate(ROUTES.TABLE.DESCRIBE, { tableName }); 12 | const { data } = await axios.get(url); 13 | 14 | return data.Table; 15 | } 16 | 17 | export async function getTableTTL(tableName: string) { 18 | const url = interpolate(ROUTES.TABLE.TTL, { tableName }); 19 | const { data } = await axios.get(url); 20 | 21 | return data.TimeToLiveDescription; 22 | } 23 | 24 | export async function updateTableTTL(tableName: string, body: object) { 25 | const url = interpolate(ROUTES.TABLE.TTL, { tableName }); 26 | const { data } = await axios.put(url, body); 27 | 28 | return data.TimeToLiveDescription; 29 | } 30 | 31 | export async function createTable(body: object) { 32 | const url = ROUTES.TABLE.CREATE; 33 | const { data } = await axios.post(url, body); 34 | 35 | return data; 36 | } 37 | 38 | export async function deleteTable(tableName: string) { 39 | const url = interpolate(ROUTES.TABLE.DELETE, { tableName }); 40 | const { data } = await axios.delete(url); 41 | 42 | return data; 43 | } 44 | 45 | export async function truncateTable(tableName: string) { 46 | const url = interpolate(ROUTES.TABLE.TRUNCATE, { tableName }); 47 | const { data } = await axios.put(url); 48 | 49 | return data; 50 | } 51 | 52 | export async function updateTable(tableName: string, body: object) { 53 | const url = interpolate(ROUTES.TABLE.UPDATE, { tableName }); 54 | const { data } = await axios.put(url, body); 55 | 56 | return data; 57 | } 58 | 59 | export async function getRemoteTables(body: object) { 60 | const { data } = await axios.post(ROUTES.DATABASE.ALL, body); 61 | 62 | return data; 63 | } 64 | 65 | export async function restoreTables(uid: string, body: object) { 66 | const url = interpolate(ROUTES.DATABASE.RESTORE, { uid }); 67 | const { data } = await axios.post(url, body); 68 | 69 | return data; 70 | } 71 | -------------------------------------------------------------------------------- /app/src/store/dynamodb.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | 3 | const state = reactive({ 4 | Limit: 50, 5 | IndexName: null, 6 | // 7 | FilterExpression: null, 8 | KeyConditionExpression: null, 9 | // 10 | ExclusiveStartKey: null, 11 | ExpressionAttributeNames: null, 12 | ExpressionAttributeValues: null, 13 | // 14 | ScanIndexForward: null, 15 | }); 16 | 17 | const setters = { 18 | init: ({ 19 | IndexName = null, 20 | FilterExpression = null, 21 | LastEvaluatedKey = null, 22 | KeyConditionExpression = null, 23 | ExpressionAttributeNames = null, 24 | ExpressionAttributeValues = null, 25 | ScanIndexForward = null, 26 | }) => { 27 | state.IndexName = IndexName; 28 | state.FilterExpression = FilterExpression; 29 | state.ExclusiveStartKey = LastEvaluatedKey; 30 | state.KeyConditionExpression = KeyConditionExpression; 31 | state.ExpressionAttributeNames = ExpressionAttributeNames; 32 | state.ExpressionAttributeValues = ExpressionAttributeValues; 33 | state.ScanIndexForward = ScanIndexForward; 34 | }, 35 | setLimit: (limit: number) => { 36 | state.Limit = limit; 37 | }, 38 | setIndexName: (indexName: string) => { 39 | state.IndexName = indexName; 40 | }, 41 | setExclusiveStartKey: (exclusiveStartKey: object) => { 42 | state.ExclusiveStartKey = exclusiveStartKey; 43 | }, 44 | 45 | reset: () => { 46 | // state.IndexName = null; 47 | state.FilterExpression = null; 48 | // state.ExclusiveStartKey = null; 49 | state.KeyConditionExpression = null; 50 | state.ExpressionAttributeNames = null; 51 | state.ExpressionAttributeValues = null; 52 | state.ScanIndexForward = null; 53 | }, 54 | }; 55 | 56 | export default { 57 | state, 58 | setters, 59 | }; 60 | -------------------------------------------------------------------------------- /app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import ui from "@/store/ui"; 2 | 3 | import item from "@/store/item"; 4 | import table from "@/store/table"; 5 | 6 | import dynamodb from "@/store/dynamodb"; 7 | 8 | export default { 9 | ui, 10 | item, 11 | table, 12 | dynamodb, 13 | }; 14 | -------------------------------------------------------------------------------- /app/src/store/item.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | 3 | const state = reactive({}); 4 | 5 | export default { 6 | state, 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/store/table.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | 3 | const state = reactive({ 4 | Table: {}, 5 | TableNames: [] as string[], 6 | }); 7 | 8 | const setters = { 9 | setTableNames: (tableNames: string[]) => { 10 | state.TableNames = tableNames; 11 | }, 12 | setTable: (table: object) => { 13 | state.Table = table; 14 | }, 15 | }; 16 | 17 | const getters = { 18 | getTable: () => { 19 | return state.Table; 20 | }, 21 | }; 22 | 23 | export default { 24 | state, 25 | setters, 26 | getters, 27 | }; 28 | -------------------------------------------------------------------------------- /app/src/store/ui.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { isEmpty } from "lodash"; 3 | import { generateTableHeaders } from "@/utils/table"; 4 | 5 | const state = reactive({ 6 | table: { 7 | page: 1, 8 | rows: [], 9 | headers: [] as string[], 10 | selectedRows: 0, 11 | count: 0, 12 | scannedCount: 0, 13 | }, 14 | isLoading: false, 15 | }); 16 | 17 | const setters = { 18 | setIsLoading: (isLoading: boolean) => { 19 | state.isLoading = isLoading; 20 | }, 21 | setTable: (table: object, items: [], count = 0, scannedCount = 0) => { 22 | state.table.rows = items; 23 | state.table.headers = generateTableHeaders(items, table); 24 | 25 | if (!isEmpty(table)) { 26 | state.table.count += count; 27 | state.table.scannedCount += scannedCount; 28 | } else { 29 | state.table.count = 0; 30 | state.table.scannedCount = 0; 31 | } 32 | }, 33 | setPage: (page: number) => { 34 | state.table.page = page; 35 | }, 36 | setSelectedRows: (selectedRows: number) => { 37 | state.table.selectedRows = selectedRows; 38 | }, 39 | }; 40 | 41 | export default { 42 | state, 43 | setters, 44 | }; 45 | -------------------------------------------------------------------------------- /app/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function interpolate(str: string, params: object): string { 2 | let formattedString = str; 3 | 4 | for (const [key, value] of Object.entries(params)) { 5 | formattedString = formattedString.replace(`:${key}`, value); 6 | } 7 | 8 | return formattedString; 9 | } 10 | 11 | export function generateString(length = 5) { 12 | const characters = "abcdefghijklmnopqrstuvwxyz"; 13 | 14 | let result = ""; 15 | for (let i = 0; i < length; i++) { 16 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 17 | } 18 | 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /app/src/utils/table.ts: -------------------------------------------------------------------------------- 1 | import { generateString } from "./string"; 2 | 3 | export function generateTableHeaders(items = [], { KeySchema = [] }) { 4 | const hashKey = KeySchema.find(({ KeyType }) => KeyType === "HASH") ?? {}; 5 | const rangeKey = KeySchema.find(({ KeyType }) => KeyType === "RANGE") ?? {}; 6 | 7 | const allHeaders = new Set(); 8 | 9 | items.forEach((item) => { 10 | Object.keys(item).forEach(allHeaders.add, allHeaders); 11 | }); 12 | 13 | const headers = Array.from(allHeaders) 14 | .filter((key) => ![hashKey.AttributeName, rangeKey.AttributeName].includes(key)) 15 | .sort(); 16 | 17 | if (rangeKey.AttributeName) { 18 | headers.unshift(rangeKey.AttributeName); 19 | } 20 | 21 | headers.unshift(hashKey.AttributeName); 22 | 23 | return headers; 24 | } 25 | 26 | function getKeySchemaWithAttributeType({ indexName, table }) { 27 | let keySchema = []; 28 | 29 | if (indexName === table.TableName) { 30 | keySchema = table.KeySchema ?? []; 31 | } else { 32 | const secondaryIndex = [...(table.GlobalSecondaryIndexes ?? []), ...(table.LocalSecondaryIndexes ?? [])]?.find( 33 | ({ IndexName }: { IndexName: string }) => IndexName === indexName, 34 | ); 35 | keySchema = secondaryIndex?.KeySchema ?? []; 36 | } 37 | 38 | const keySchemaWithAttributeType = keySchema.map((key) => { 39 | const { AttributeType } = table.AttributeDefinitions.find( 40 | (attributeDefinition: any) => key.AttributeName === attributeDefinition.AttributeName, 41 | ); 42 | 43 | return { 44 | ...key, 45 | AttributeType, 46 | }; 47 | }); 48 | 49 | const pk = keySchemaWithAttributeType.find(({ KeyType }) => KeyType === "HASH"); 50 | const sk = keySchemaWithAttributeType.find(({ KeyType }) => KeyType === "RANGE"); 51 | 52 | return { 53 | pk, 54 | sk, 55 | keySchemaWithAttributeType, 56 | }; 57 | } 58 | 59 | function valueOf(value, AttributeType) { 60 | if (AttributeType === "S") return String(value); 61 | if (AttributeType === "N") return parseInt(value); 62 | if (AttributeType === "BOOL") return String(value) === "true"; 63 | } 64 | 65 | export function generateDynamodbParameters({ table, indexName, parameters }) { 66 | const { pk, sk } = getKeySchemaWithAttributeType({ table, indexName }); 67 | 68 | if (!pk || !parameters?.keys) return; 69 | 70 | let skConditionExpression = ""; 71 | let pkConditionExpression = ""; 72 | const attributeNames: any = {}; 73 | const attributeValues: any = {}; 74 | 75 | // PK 76 | if (pk && parameters.keys.pk.value) { 77 | pkConditionExpression = `#${pk.AttributeName} ${parameters.keys.pk.condition} :${pk.AttributeName}`; 78 | 79 | attributeNames[`#${pk.AttributeName}`] = pk.AttributeName; 80 | attributeValues[`:${pk.AttributeName}`] = valueOf(parameters.keys.pk.value, pk.AttributeType); 81 | } 82 | 83 | // SK 84 | if (pk && parameters.keys.sk) { 85 | if (sk && parameters.keys.sk.value1) { 86 | attributeNames[`#${sk.AttributeName}`] = sk.AttributeName; 87 | 88 | // begins_with 89 | if (parameters.keys.sk.condition === "begins_with") { 90 | skConditionExpression = `begins_with(#${sk.AttributeName}, :${sk.AttributeName})`; 91 | attributeValues[`:${sk.AttributeName}`] = valueOf(parameters.keys.sk.value1, sk.AttributeType); 92 | } 93 | // between 94 | else if (parameters.keys.sk.condition === "between") { 95 | skConditionExpression = `#${sk.AttributeName} between :${sk.AttributeName}1 and :${sk.AttributeName}2`; 96 | attributeValues[`:${sk.AttributeName}1`] = valueOf(parameters.keys.sk.value1, sk.AttributeType); 97 | attributeValues[`:${sk.AttributeName}2`] = valueOf(parameters.keys.sk.value2, sk.AttributeType); 98 | } 99 | // =, <=, <, >=, > 100 | else { 101 | skConditionExpression = `#${sk.AttributeName} ${parameters.keys.sk.condition} :${sk.AttributeName}`; 102 | attributeValues[`:${sk.AttributeName}`] = valueOf(parameters.keys.sk.value1, sk.AttributeType); 103 | } 104 | } 105 | } 106 | 107 | const filters = []; 108 | let scan = parameters.scan ?? []; 109 | 110 | scan = scan 111 | .filter(({ name }) => !!name) 112 | .map((filter) => ({ 113 | ...filter, 114 | salt: generateString(), 115 | })); 116 | 117 | for (const filter of scan) { 118 | let { name } = filter; 119 | const { value, type, condition, value2, salt } = filter; 120 | 121 | const attributes = name.split("."); 122 | name = name.replaceAll(".", ".#"); 123 | 124 | let expression = ""; 125 | if (!condition) continue; 126 | 127 | if (["attribute_exists", "attribute_not_exists"].includes(condition)) { 128 | expression = `${condition}(#${name})`; 129 | attributes.map((name) => (attributeNames[`#${name}`] = name)); 130 | filters.push(expression); 131 | continue; 132 | } 133 | 134 | // 135 | if (!name || !type || (condition === "between" && !value2)) continue; 136 | 137 | if (["begins_with", "contains", "not contains"].includes(condition)) { 138 | expression = `${condition}(#${name}, :${salt})`; 139 | attributes.map((name) => (attributeNames[`#${name}`] = name)); 140 | attributeValues[`:${salt}`] = valueOf(value, type); 141 | } else if ("between" === condition) { 142 | expression = `#${name} between :${salt}1 and :${salt}2`; 143 | attributes.map((name) => (attributeNames[`#${name}`] = name)); 144 | attributeValues[`:${salt}1`] = valueOf(value, type); 145 | attributeValues[`:${salt}2`] = valueOf(value2, type); 146 | } else { 147 | expression = `#${name} ${condition} :${salt}`; 148 | attributes.map((name) => (attributeNames[`#${name}`] = name)); 149 | attributeValues[`:${salt}`] = valueOf(value, type); 150 | } 151 | 152 | filters.push(expression); 153 | } 154 | 155 | const IndexName = indexName === table.TableName ? null : indexName; 156 | 157 | const params = { 158 | // IndexName 159 | ...(IndexName && { IndexName }), 160 | 161 | // KeyConditionExpression 162 | ...(pk && 163 | parameters.keys.pk.value && { 164 | KeyConditionExpression: 165 | sk && parameters.keys.sk?.value1 166 | ? [pkConditionExpression, skConditionExpression].join(" and ") 167 | : pkConditionExpression, 168 | }), 169 | 170 | // FilterExpression 171 | ...(Object.keys(filters).length && { 172 | FilterExpression: filters.join(" and "), 173 | }), 174 | 175 | // ExpressionAttributeNames 176 | ...(Object.keys(attributeNames).length && { 177 | ExpressionAttributeNames: attributeNames, 178 | }), 179 | 180 | // ExpressionAttributeValues 181 | ...(Object.keys(attributeValues).length && { 182 | ExpressionAttributeValues: attributeValues, 183 | }), 184 | 185 | // ScanIndexForward 186 | ScanIndexForward: Boolean(parameters.keys.sk.scanIndexForward), 187 | }; 188 | 189 | if (params.ScanIndexForward) { 190 | delete params.ScanIndexForward; 191 | } 192 | 193 | return params; 194 | } 195 | 196 | export function generateDynamodbIndexParameters({ indices = [], deleteIndices = [] }) { 197 | const parameters = {}; 198 | 199 | const gsis = indices.filter(({ readOnly }) => !readOnly); 200 | 201 | if (gsis.length || deleteIndices.length) { 202 | parameters.GlobalSecondaryIndexUpdates = []; 203 | } 204 | 205 | if (deleteIndices.length) { 206 | const deletes = deleteIndices.map((indexName) => ({ 207 | Delete: { IndexName: indexName }, 208 | })); 209 | 210 | parameters.GlobalSecondaryIndexUpdates.push(...deletes); 211 | } 212 | 213 | if (gsis.length) { 214 | parameters.AttributeDefinitions = []; 215 | 216 | const creates = gsis.map(({ name, pk = { name: "", type: "" }, sk = { name: "", type: "" } }) => { 217 | const index = { 218 | IndexName: name, 219 | Projection: { 220 | ProjectionType: "ALL", 221 | }, 222 | KeySchema: [ 223 | { 224 | AttributeName: pk.name, 225 | KeyType: "HASH", 226 | }, 227 | ...(sk.name.trim() 228 | ? [ 229 | { 230 | AttributeName: sk.name, 231 | KeyType: "RANGE", 232 | }, 233 | ] 234 | : []), 235 | ], 236 | ProvisionedThroughput: { 237 | ReadCapacityUnits: 5, 238 | WriteCapacityUnits: 5, 239 | }, 240 | }; 241 | 242 | const pkExists = parameters.AttributeDefinitions.find(({ AttributeName }) => AttributeName === pk.name); 243 | 244 | const skExists = parameters.AttributeDefinitions.find(({ AttributeName }) => AttributeName === sk.name); 245 | 246 | if (!pkExists) { 247 | parameters.AttributeDefinitions.push({ 248 | AttributeName: pk.name, 249 | AttributeType: pk.type, 250 | }); 251 | } 252 | 253 | if (!skExists && sk.name.trim()) { 254 | parameters.AttributeDefinitions.push({ 255 | AttributeName: sk.name, 256 | AttributeType: sk.type, 257 | }); 258 | } 259 | 260 | return index; 261 | }); 262 | 263 | parameters.GlobalSecondaryIndexUpdates.push( 264 | ...creates.map((index) => ({ 265 | Create: index, 266 | })), 267 | ); 268 | } 269 | 270 | return parameters; 271 | } 272 | 273 | export function generateDynamodbTableParameters({ 274 | name = "", 275 | throughput: { read = 5, write = 5 }, 276 | keySchema: { pk = { name: "", type: "" }, sk = { name: "", type: "" } }, 277 | indices = [], 278 | }) { 279 | const parameters = { 280 | TableName: name, 281 | ProvisionedThroughput: { 282 | ReadCapacityUnits: read, 283 | WriteCapacityUnits: write, 284 | }, 285 | KeySchema: [ 286 | { 287 | AttributeName: pk.name, 288 | KeyType: "HASH", 289 | }, 290 | ...(sk.name.trim() 291 | ? [ 292 | { 293 | AttributeName: sk.name, 294 | KeyType: "RANGE", 295 | }, 296 | ] 297 | : []), 298 | ], 299 | AttributeDefinitions: [ 300 | { 301 | AttributeName: pk.name, 302 | AttributeType: pk.type, 303 | }, 304 | ...(sk.name.trim() 305 | ? [ 306 | { 307 | AttributeName: sk.name, 308 | AttributeType: sk.type, 309 | }, 310 | ] 311 | : []), 312 | ], 313 | /* 314 | GlobalSecondaryIndexes: [ 315 | { 316 | IndexName: "", 317 | Projection: { 318 | ProjectionType: "ALL", 319 | }, 320 | KeySchema: [ 321 | { 322 | AttributeName: "", 323 | KeyType: "HASH", 324 | }, 325 | { 326 | AttributeName: "", 327 | KeyType: "RANGE", 328 | }, 329 | ], 330 | ProvisionedThroughput: { 331 | ReadCapacityUnits: 5, 332 | WriteCapacityUnits: 5, 333 | }, 334 | }, 335 | ], 336 | LocalSecondaryIndexes: [ 337 | { 338 | IndexName: "", 339 | KeySchema: [ 340 | { 341 | AttributeName: "", 342 | KeyType: "HASH", 343 | }, 344 | { 345 | AttributeName: "", 346 | KeyType: "RANGE", 347 | }, 348 | ], 349 | Projection: { 350 | ProjectionType: "ALL", 351 | }, 352 | }, 353 | ], 354 | */ 355 | }; 356 | 357 | const gsis = indices.filter(({ type }) => type === "GSI"); 358 | const lsis = indices.filter(({ type }) => type === "LSI"); 359 | 360 | if (gsis.length) { 361 | parameters["GlobalSecondaryIndexes"] = gsis.map( 362 | ({ name, pk = { name: "", type: "" }, sk = { name: "", type: "" } }) => { 363 | const index = { 364 | IndexName: name, 365 | Projection: { 366 | ProjectionType: "ALL", 367 | }, 368 | KeySchema: [ 369 | { 370 | AttributeName: pk.name, 371 | KeyType: "HASH", 372 | }, 373 | ...(sk.name.trim() 374 | ? [ 375 | { 376 | AttributeName: sk.name, 377 | KeyType: "RANGE", 378 | }, 379 | ] 380 | : []), 381 | ], 382 | ProvisionedThroughput: { 383 | ReadCapacityUnits: read, 384 | WriteCapacityUnits: write, 385 | }, 386 | }; 387 | 388 | const pkExists = parameters.AttributeDefinitions.find(({ AttributeName }) => AttributeName === pk.name); 389 | 390 | const skExists = parameters.AttributeDefinitions.find(({ AttributeName }) => AttributeName === sk.name); 391 | 392 | if (!pkExists) { 393 | parameters.AttributeDefinitions.push({ 394 | AttributeName: pk.name, 395 | AttributeType: pk.type, 396 | }); 397 | } 398 | 399 | if (!skExists && sk.name.trim()) { 400 | parameters.AttributeDefinitions.push({ 401 | AttributeName: sk.name, 402 | AttributeType: sk.type, 403 | }); 404 | } 405 | 406 | return index; 407 | }, 408 | ); 409 | } 410 | 411 | if (lsis.length) { 412 | parameters["LocalSecondaryIndexes"] = lsis.map(({ name, sk = { name: "", type: "" } }) => { 413 | const index = { 414 | IndexName: name, 415 | Projection: { 416 | ProjectionType: "ALL", 417 | }, 418 | KeySchema: [ 419 | { 420 | AttributeName: pk.name, 421 | KeyType: "HASH", 422 | }, 423 | ...(sk.name.trim() 424 | ? [ 425 | { 426 | AttributeName: sk.name, 427 | KeyType: "RANGE", 428 | }, 429 | ] 430 | : []), 431 | ], 432 | }; 433 | 434 | const skExists = parameters.AttributeDefinitions.find(({ AttributeName }) => AttributeName === sk.name); 435 | 436 | if (!skExists) { 437 | parameters.AttributeDefinitions.push({ 438 | AttributeName: sk.name, 439 | AttributeType: sk.type, 440 | }); 441 | } 442 | 443 | return index; 444 | }); 445 | } 446 | 447 | return parameters; 448 | } 449 | -------------------------------------------------------------------------------- /app/src/views/items/CreateItem.vue: -------------------------------------------------------------------------------- 1 |