├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── new-feature.md ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── getting_started │ │ ├── configure.mdx │ │ ├── import.mdx │ │ ├── installation.mdx │ │ ├── introduction.mdx │ │ └── quick_start.mdx │ ├── guide │ │ ├── condition.mdx │ │ ├── dynamode.mdx │ │ ├── entity │ │ │ ├── decorators.mdx │ │ │ └── modeling.mdx │ │ ├── managers │ │ │ ├── entityManager.mdx │ │ │ └── tableManager.mdx │ │ ├── query.mdx │ │ ├── scan.mdx │ │ ├── stream.mdx │ │ └── transactions.mdx │ └── other │ │ └── version_requirements.mdx ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ ├── pages │ │ ├── index.jsx │ │ └── styles.module.css │ └── theme │ │ ├── BeforeAndAfter │ │ ├── index.jsx │ │ └── styles.module.css │ │ ├── Footer │ │ ├── index.js │ │ └── styles.module.css │ │ └── TutorialStep │ │ ├── index.jsx │ │ └── styles.module.css └── static │ ├── fonts │ ├── Aeonik-Air.woff │ ├── Aeonik-Air.woff2 │ ├── Aeonik-AirItalic.woff │ ├── Aeonik-AirItalic.woff2 │ ├── Aeonik-Black.woff │ ├── Aeonik-Black.woff2 │ ├── Aeonik-BlackItalic.woff │ ├── Aeonik-BlackItalic.woff2 │ ├── Aeonik-Bold.woff │ ├── Aeonik-Bold.woff2 │ ├── Aeonik-BoldItalic.woff │ ├── Aeonik-BoldItalic.woff2 │ ├── Aeonik-Light.woff │ ├── Aeonik-Light.woff2 │ ├── Aeonik-LightItalic.woff │ ├── Aeonik-LightItalic.woff2 │ ├── Aeonik-Medium.woff │ ├── Aeonik-Medium.woff2 │ ├── Aeonik-MediumItalic.woff │ ├── Aeonik-MediumItalic.woff2 │ ├── Aeonik-Regular.woff │ ├── Aeonik-Regular.woff2 │ ├── Aeonik-RegularItalic.woff │ ├── Aeonik-RegularItalic.woff2 │ ├── Aeonik-Thin.woff │ ├── Aeonik-Thin.woff2 │ ├── Aeonik-ThinItalic.woff │ └── Aeonik-ThinItalic.woff2 │ ├── img │ ├── amazon-ddb.png │ ├── banner.png │ ├── dynamodb-logo.svg │ ├── dynamode-logo.png │ ├── dynamode.svg │ ├── engine_frame.svg │ ├── fav_192x192.png │ ├── github-logo.svg │ ├── swm-logo-small.svg │ └── swm-logo.svg │ └── robots.txt ├── examples ├── AllPossibleProperties │ ├── methods │ │ ├── batchDelete.ts │ │ ├── batchGet.ts │ │ ├── batchPut.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── put.ts │ │ ├── query.ts │ │ ├── scan.ts │ │ ├── transactionGet.ts │ │ ├── transactionWrite.ts │ │ └── update.ts │ └── model.ts ├── Inheritance │ ├── methods │ │ ├── get.ts │ │ └── put.ts │ └── model.ts ├── KeyValue │ ├── methods │ │ ├── batchDelete.ts │ │ ├── batchGet.ts │ │ ├── batchPut.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── put.ts │ │ ├── query.ts │ │ ├── scan.ts │ │ ├── transactionGet.ts │ │ ├── transactionWrite.ts │ │ └── update.ts │ └── model.ts ├── ReservedWord │ ├── methods │ │ ├── batchDelete.ts │ │ ├── batchGet.ts │ │ ├── batchPut.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── put.ts │ │ ├── query.ts │ │ ├── scan.ts │ │ ├── transactionGet.ts │ │ ├── transactionWrite.ts │ │ └── update.ts │ └── model.ts ├── User │ ├── methods │ │ ├── batchDelete.ts │ │ ├── batchGet.ts │ │ ├── batchPut.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── put.ts │ │ ├── query.ts │ │ ├── scan.ts │ │ ├── transactionGet.ts │ │ ├── transactionWrite.ts │ │ └── update.ts │ └── model.ts ├── docker-compose.yml ├── playground.ts └── tsconfig.json ├── lib ├── condition │ ├── index.ts │ └── types.ts ├── decorators │ ├── helpers │ │ ├── customName.ts │ │ ├── dates.ts │ │ ├── decorateAttribute.ts │ │ ├── gsi.ts │ │ ├── lsi.ts │ │ ├── other.ts │ │ ├── prefixSuffix.ts │ │ └── primaryKey.ts │ ├── index.ts │ └── types.ts ├── dynamode │ ├── converter │ │ └── index.ts │ ├── ddb │ │ └── index.ts │ ├── index.ts │ ├── separator │ │ └── index.ts │ └── storage │ │ ├── helpers │ │ └── validator.ts │ │ ├── index.ts │ │ └── types.ts ├── entity │ ├── entityManager.ts │ ├── helpers │ │ ├── buildExpressions.ts │ │ ├── buildOperators.ts │ │ ├── converters.ts │ │ ├── returnValues.ts │ │ └── transformValues.ts │ ├── index.ts │ └── types.ts ├── index.ts ├── module.ts ├── query │ ├── index.ts │ └── types.ts ├── retriever │ └── index.ts ├── scan │ ├── index.ts │ └── types.ts ├── stream │ ├── index.ts │ └── types.ts ├── table │ ├── helpers │ │ ├── builders.ts │ │ ├── converters.ts │ │ ├── definitions.ts │ │ ├── indexes.ts │ │ ├── schema.ts │ │ ├── utils.ts │ │ └── validator.ts │ ├── index.ts │ └── types.ts ├── transactionGet │ ├── index.ts │ └── types.ts ├── transactionWrite │ ├── index.ts │ └── types.ts └── utils │ ├── ExpressionBuilder.ts │ ├── constants.ts │ ├── converter.ts │ ├── errors.ts │ ├── helpers.ts │ ├── index.ts │ └── types.ts ├── package-lock.json ├── package.json ├── tests ├── e2e │ ├── condition.test.ts │ ├── entity │ │ ├── batchDelete.test.ts │ │ ├── batchGet.test.ts │ │ ├── batchPut.test.ts │ │ ├── create.test.ts │ │ ├── delete.test.ts │ │ ├── get.test.ts │ │ ├── put.test.ts │ │ ├── query.test.ts │ │ └── update.test.ts │ ├── indexes │ │ └── query.test.ts │ └── mockEntityFactory.ts ├── fixtures │ ├── TestIndex.ts │ └── TestTable.ts ├── tsconfig.json ├── types │ ├── EntityKey.test.ts │ └── EntityValue.test.ts └── unit │ ├── condition │ └── index.test.ts │ ├── decorators │ ├── helpers │ │ ├── customName.test.ts │ │ ├── dates.test.ts │ │ ├── decorateAttribute.test.ts │ │ ├── gsi.test.ts │ │ ├── lsi.test.ts │ │ ├── other.test.ts │ │ ├── prefixSuffix.test.ts │ │ └── primaryKey.test.ts │ └── index.test.ts │ ├── dynamode │ ├── helpers.test.ts │ └── index.test.ts │ ├── entity │ ├── helpers │ │ ├── buildExpressions.test.ts │ │ ├── buildOperators.test.ts │ │ ├── converters.test.ts │ │ ├── returnValues.test.ts │ │ └── transformValues.test.ts │ └── index.test.ts │ ├── query │ └── index.test.ts │ ├── retriever │ └── index.test.ts │ ├── scan │ └── index.test.ts │ ├── stream │ └── index.test.ts │ ├── table │ ├── helpers │ │ ├── builders.test.ts │ │ ├── coverters.test.ts │ │ ├── definitions.test.ts │ │ ├── fixtures.ts │ │ ├── indexes.test.ts │ │ ├── schema.test.ts │ │ ├── utils.test.ts │ │ └── validator.test.ts │ └── index.test.ts │ ├── transactionGet │ └── index.test.ts │ ├── transactionWrite │ └── index.test.ts │ └── utils │ ├── ExpressionBuilder.test.ts │ ├── constants.test.ts │ ├── converter.test.ts │ ├── errors.test.ts │ └── helpers.test.ts ├── tsconfig.json ├── tsconfig.typecheck.json └── vitest.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["import", "simple-import-sort", "@typescript-eslint", "unused-imports"], 8 | "extends": [ 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:import/typescript", 13 | "prettier" 14 | ], 15 | "env": { 16 | "node": true 17 | }, 18 | "rules": { 19 | "@typescript-eslint/no-explicit-any": ["off"], 20 | "import/no-unresolved": "error", 21 | "simple-import-sort/imports": [ 22 | "error", 23 | { 24 | "groups": [ 25 | ["^\\w"], 26 | // Internal packages. 27 | ["^@/?\\w"], 28 | // Parent imports. Put `..` last. 29 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"], 30 | // Other relative imports. Put same-folder imports and `.` last. 31 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] 32 | ] 33 | } 34 | ], 35 | "no-unused-vars": "off", 36 | "unused-imports/no-unused-imports": "error", 37 | "unused-imports/no-unused-vars": [ 38 | "warn", 39 | { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } 40 | ], 41 | "@typescript-eslint/consistent-type-definitions": ["error", "type"] 42 | }, 43 | "settings": { 44 | "import/parsers": { 45 | "@typescript-eslint/parser": [".ts"] 46 | }, 47 | "import/resolver": { 48 | "typescript": { 49 | "alwaysTryTypes": true, 50 | "project": "./tsconfig.json" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Summary: 13 | 14 | 15 | 16 | ### Code sample: 17 | 18 | #### Model 19 | ```ts 20 | // Code here 21 | ``` 22 | 23 | #### General 24 | ```ts 25 | // Code here 26 | ``` 27 | 28 | ### Action performed: 29 | 30 | 31 | 32 | ### Expected result: 33 | 34 | 35 | 36 | ### Actual result: 37 | 38 | 39 | 40 | ### Environment: 41 | 42 | Operating System & version: 43 | Node.js version (`node -v`): 44 | NPM version: (`npm -v`): 45 | Dynamode version: 46 | 47 | 48 | ### Other: 49 | - [ ] I have read through the Dynamode documentation before posting this issue 50 | - [ ] I have searched through the GitHub issues (including closed issues) and pull requests to ensure this issue has not already been raised before 51 | - [ ] I have searched the internet to ensure this issue hasn't been raised or answered before -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: new feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Summary: 13 | 14 | 15 | 16 | ### Code sample: 17 | ```ts 18 | // Code here 19 | ``` 20 | 21 | ### Other: 22 | - [ ] I have read through the Dynamode documentation before posting this issue 23 | - [ ] I have searched through the GitHub issues (including closed issues) and pull requests to ensure this feature has not already been suggested before -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### Summary: 3 | 4 | 5 | ### Code sample: 6 | 7 | #### Model 8 | ```ts 9 | // Code here 10 | ``` 11 | 12 | #### General 13 | ```ts 14 | // Code here 15 | ``` 16 | 17 | 18 | ### GitHub linked issue: 19 | 20 | #---- 21 | 22 | 23 | ### Type of change: 24 | - [ ] Bug fix 25 | - [ ] Feature implementation 26 | - [ ] Documentation improvement 27 | - [ ] Testing improvement 28 | - [ ] Something not listed here 29 | 30 | 31 | ### Is this a breaking change? 32 | - [ ] YES 🚨 33 | - [ ] No 34 | 35 | ### Other: 36 | - [ ] I have searched through the GitHub pull requests to ensure this PR has not already been submitted 37 | - [ ] I have updated the Dynamode documentation (if required) 38 | - [ ] I have added/updated the Dynamode test cases (if required) 39 | - [ ] I agree that all changes made in this pull request may be distributed and are made available in accordance with the [Dynamode License](../LICENSE). 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test_build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node: [18, 20] 16 | name: Node ${{ matrix.node }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Setup Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node }} 24 | registry-url: 'https://registry.npmjs.org' 25 | - name: Install dependencies ⏳ 26 | run: npm ci 27 | - name: Build 🔧 28 | run: npm run build 29 | - name: Typecheck 🏷️ 30 | run: npm run typecheck 31 | - name: Lint 🧹 32 | run: npm run lint 33 | - name: Test Unit 🧪 34 | run: npm run test 35 | - name: Test types 🛠️ 36 | run: npm run test:types 37 | - name: Setup DynamoDB Local 38 | uses: rrainn/dynamodb-action@v3.0.0 39 | - name: Sleep for 10 seconds 40 | run: sleep 10s 41 | shell: bash 42 | - name: Test E2E ⚙️ 43 | run: npm run test:e2e 44 | - name: Coverage 📝 45 | run: npm run coverage 46 | - name: Coveralls 47 | uses: coverallsapp/github-action@1.1.3 48 | continue-on-error: true 49 | with: 50 | path-to-lcov: ./coverage/lcov.info 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | pre_deploy: 10 | name: Deploy to GitHub Pages 11 | runs-on: ubuntu-latest 12 | outputs: 13 | docs_changed: ${{ steps.docs-files.outputs.any_changed }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Get changed files in the docs folder 20 | id: docs-files 21 | uses: tj-actions/changed-files@v31 22 | with: 23 | files: docs/** 24 | 25 | deploy: 26 | runs-on: ubuntu-latest 27 | needs: [pre_deploy] 28 | if: needs.pre_deploy.outputs.docs_changed == 'true' 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | - name: Setup Node 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: 18 36 | registry-url: 'https://registry.npmjs.org' 37 | - name: Install dependencies ⏳ 38 | working-directory: ./docs 39 | run: npm ci 40 | - name: Build 🔧 41 | working-directory: ./docs 42 | run: npm run build 43 | - name: Deploy docs to GitHub Pages 44 | uses: peaceiris/actions-gh-pages@v3 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | publish_dir: ./docs/build 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Setup Node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Install dependencies ⏳ 19 | run: npm ci 20 | - name: Lint 🧹 21 | run: npm run lint 22 | - name: Test Unit 🧪 23 | run: npm run test 24 | - name: Test types 🛠️ 25 | run: npm run test:types 26 | - name: Setup DynamoDB Local 27 | uses: rrainn/dynamodb-action@v2.0.0 28 | - name: Sleep for 10 seconds 29 | run: sleep 10s 30 | shell: bash 31 | - name: Test E2E ⚙️ 32 | run: npm run test:e2e 33 | - name: Coverage 📝 34 | run: npm run coverage 35 | - name: Build 🔧 36 | run: npm run build 37 | - name: Publish package on NPM 📦 38 | run: cd dist && npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | npm-debug.log* 6 | npm-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # MacOs DS Store files 58 | .DS_Store 59 | 60 | .idea 61 | dist/ 62 | examples/docker/ 63 | 64 | # Vitest 65 | tsconfig.vitest-temp.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["dynamode", "unmarshall"], 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "files.associations": { 7 | "*.mdx": "markdown" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Software Mansion 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. -------------------------------------------------------------------------------- /docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '../.eslintrc.js', 4 | }; 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/getting_started/configure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configure | Dynamode 3 | description: Configure 4 | sidebar_label: Configure 5 | hide_title: true 6 | --- 7 | 8 | # Configure 9 | 10 | ## AWS SDK 11 | 12 | Dynamode uses [`@aws-sdk/client-dynamodb`](https://www.npmjs.com/package/@aws-sdk/client-dynamodb) package under the hood. Therefore, it is required to authenticate with AWS so Dynamode can make requests to DynamoDB. 13 | 14 | :::caution 15 | Authenticate as early in the application life cycle as possible to prevent errors. Dynamode needs valid AWS credentials to make requests to DynamoDB. 16 | ::: 17 | 18 | ## Authenticate with environment variables 19 | 20 | You can use environment variables to authenticate. 21 | 22 | ```bash 23 | AWS_ACCESS_KEY_ID = "key-id" 24 | AWS_SECRET_ACCESS_KEY = "secret" 25 | AWS_REGION = "region" 26 | ``` 27 | 28 | ```ts 29 | import Dynamode from 'dynamode/dynamode'; 30 | // Access DynamoDB instance 31 | const ddb = Dynamode.ddb.get(); 32 | ``` 33 | 34 | ## Authenticate programmatically 35 | 36 | :::info 37 | This is recommended way of authenticating with AWS. 38 | ::: 39 | 40 | You can also instantiate DynamoDB programmatically, pass the instance to Dynamode in order to use it. 41 | 42 | ```ts 43 | import { DynamoDB } from '@aws-sdk/client-dynamodb'; 44 | import Dynamode from 'dynamode/dynamode'; 45 | 46 | // Instantiate DynamoDB 47 | const ddb = new DynamoDB({ 48 | credentials: { 49 | accessKeyId: 'key-id', 50 | secretAccessKey: 'secret', 51 | }, 52 | region: 'region', 53 | }); 54 | 55 | // Pass DynamoDB instance to Dynamode 56 | Dynamode.ddb.set(ddb); 57 | ``` 58 | 59 | ## Authenticate locally 60 | 61 | To setup [DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) locally. Check out [`docker-compose.yml`](https://www.github.com/blazejkustra/dynamode/blob/main/examples/docker-compose.yml) file and [`examples/`](https://www.github.com/blazejkustra/dynamode/blob/main/examples/) catalog. Once DynamoDB local server is running on `'http://localhost:8000'` you can use the following command. 62 | 63 | ```ts 64 | import Dynamode from 'dynamode/dynamode'; 65 | 66 | // Local DynamoDB instance 67 | const ddb = Dynamode.ddb.local(); 68 | ``` 69 | 70 | In case your local DynamoDB server is running at a different url you can pass that in as an argument. 71 | 72 | ```ts 73 | const ddb = Dynamode.ddb.local('http://localhost:2137'); 74 | ``` 75 | 76 | Read more about `Dynamode` class [here](/docs/guide/dynamode). 77 | -------------------------------------------------------------------------------- /docs/docs/getting_started/import.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Import | Dynamode 3 | description: Import 4 | sidebar_label: Import 5 | hide_title: true 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | # Import 12 | 13 | In order to use **Dynamode** you need to either `import` or `require` it in your project. 14 | 15 | 16 | 17 | 18 | ```ts 19 | import dynamode from 'dynamode'; 20 | ``` 21 | 22 | 23 | 24 | 25 | ```ts 26 | const dynamode = require('dynamode'); 27 | ``` 28 | 29 | 30 | 31 | 32 | ## Import specific classes/methods individually 33 | 34 | :::info 35 | This is recommended way of importing Dynamode. 36 | ::: 37 | 38 | ```ts 39 | import Entity from 'dynamode/entity'; 40 | import attribute from 'dynamode/decorators'; 41 | import TableManager from 'dynamode/table'; 42 | import Dynamode from 'dynamode/dynamode'; 43 | ``` 44 | 45 | ## Import the whole Dynamode library 46 | 47 | ```ts 48 | import dynamode from 'dynamode'; 49 | const { Entity, attribute, TableManager, Dynamode } = dynamode; 50 | ``` 51 | 52 | ## Import specific classes/methods inside of curly brackets 53 | 54 | ```ts 55 | import { Entity, attribute, TableManager, Dynamode } from 'dynamode'; 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/docs/getting_started/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation | Dynamode 3 | description: Installation 4 | sidebar_label: Installation 5 | hide_title: true 6 | --- 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | # Installation 12 | 13 | ## Standard 14 | 15 | To install Dynamode run the following command. 16 | 17 | 18 | 19 | 20 | ```bash 21 | npm install dynamode 22 | ``` 23 | 24 | 25 | 26 | 27 | ```bash 28 | yarn add dynamode 29 | ``` 30 | 31 | 32 | 33 | 34 | ```bash 35 | pnpm add dynamode 36 | ``` 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/docs/getting_started/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction | Dynamode 3 | description: Dynamode is a modeling tool for Amazon's DynamoDB. Its goal is to ease the use of DynamoDB without its quirks and emphasize DynamoDB advantages over other databases. 4 | sidebar_label: Introduction 5 | hide_title: true 6 | --- 7 | 8 | # Introduction 9 | 10 | ## About Dynamode 11 | 12 | Dynamode is a modeling tool for Amazon's DynamoDB. Its goal is to ease the use of DynamoDB without its quirks and emphasize DynamoDB advantages over other databases. 13 | 14 | Dynamode provides a straightforward, object-oriented class-based solution to model your data. It includes strongly typed classes and methods, query and scan builders, and much more. 15 | 16 | Dynamode is highly influenced by other ORMs/ODMs, such as [TypeORM](https://www.github.com/typeorm/typeorm), [Dynamoose](https://www.github.com/dynamoose/dynamoose) and [Mongoose](https://www.github.com/Automattic/mongoose). 17 | 18 | ## Features 19 | 20 | - Strongly typed 21 | - Easy to use with elegant-syntax 22 | - High level API 23 | - Clean object oriented model 24 | - DynamoDB single table design support 25 | - Powerful query, scan and conditions builder 26 | - AWS multi-region support 27 | - DynamoDB transactions support 28 | 29 | ## Coming soon 30 | 31 | - Migrations and automatic migrations generation. 32 | - PartiQL support 33 | - Capture DynamoDB errors and make it easier to work with 34 | 35 | ### Road map 36 | 37 | * [ ] Query that supports querying different types of entities at once with TS in mind. 38 | * [ ] Possibility to have more than one suffix/prefix 39 | * [ ] PartiQL support 40 | * [ ] Add dependsOn decorator to throw/warn when updating 41 | * [ ] Allow multiple DynamoDB instances 42 | * [ ] Capture DynamoDB errors and make it easier to work with -------------------------------------------------------------------------------- /docs/docs/other/version_requirements.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Version requirements | Dynamode 3 | description: Version requirements 4 | sidebar_label: Version requirements 5 | hide_title: true 6 | --- 7 | # Version requirements 8 | 9 | ## Node.js 10 | 11 | Dynamode might work with older versions of Node.js, but it is recommended to use at least `16.x.x`. 12 | 13 | | Dynamode version | requirements | 14 | | ---------------- | ------------------- | 15 | | `0.x.x` | `Node.js >= 16.x.x` | 16 | 17 | ## Typescript 18 | 19 | In order to get the most from dynamode it is recommended to use the latest Typescript version that is available. 20 | 21 | | Dynamode version | requirements | 22 | | ---------------- | --------------------- | 23 | | `0.x.x` | `Typescript >= 4.x.x` | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamode-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "^2.0.1", 18 | "@docusaurus/preset-classic": "^2.0.1", 19 | "@mdx-js/react": "^1.6.21", 20 | "classnames": "^2.3.1", 21 | "clsx": "^1.1.1", 22 | "react": "^17.0.1", 23 | "react-dom": "^17.0.1" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.5%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sidebar: [ 3 | { 4 | type: 'category', 5 | label: 'Getting Started', 6 | items: [ 7 | 'getting_started/introduction', 8 | 'getting_started/installation', 9 | 'getting_started/quick_start', 10 | 'getting_started/import', 11 | 'getting_started/configure', 12 | ], 13 | }, 14 | { 15 | type: 'category', 16 | label: 'Guide', 17 | items: [ 18 | { 19 | Entity: [ 20 | { 21 | type: 'doc', 22 | id: 'guide/entity/modeling', 23 | }, 24 | { 25 | type: 'doc', 26 | id: 'guide/entity/decorators', 27 | }, 28 | ], 29 | }, 30 | { 31 | Managers: [ 32 | { 33 | type: 'doc', 34 | id: 'guide/managers/tableManager', 35 | }, 36 | { 37 | type: 'doc', 38 | id: 'guide/managers/entityManager', 39 | }, 40 | ], 41 | }, 42 | 'guide/condition', 43 | 'guide/query', 44 | 'guide/scan', 45 | 'guide/transactions', 46 | 'guide/stream', 47 | 'guide/dynamode', 48 | ], 49 | }, 50 | { 51 | type: 'doc', 52 | id: 'other/version_requirements', 53 | }, 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 2rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | .buttons { 16 | display: flex; 17 | align-items: center; 18 | justify-content: flex-start; 19 | } 20 | 21 | @media screen and (max-width: 966px) { 22 | .heroBanner { 23 | padding: 0 0; 24 | text-align: center; 25 | position: relative; 26 | overflow: hidden; 27 | } 28 | 29 | .buttons { 30 | min-height: 8rem; 31 | justify-content: flex-start; 32 | } 33 | } 34 | 35 | @media screen and (max-width: 600px) { 36 | .buttons { 37 | min-height: 8rem; 38 | flex-direction: column; 39 | justify-content: space-around; 40 | } 41 | } 42 | 43 | @media screen and (max-width: 460px) { 44 | .buttons { 45 | min-height: 8rem; 46 | flex-direction: column; 47 | justify-content: space-around; 48 | margin: -14px; 49 | } 50 | } 51 | 52 | .features { 53 | display: flex; 54 | align-items: center; 55 | padding: 2rem 0; 56 | width: 100%; 57 | } 58 | 59 | .row { 60 | margin: 0 !important; 61 | } 62 | 63 | .codeBlock { 64 | text-align: left; 65 | } -------------------------------------------------------------------------------- /docs/src/theme/BeforeAndAfter/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import styles from './styles.module.css'; 4 | import clsx from 'clsx'; 5 | 6 | const BeforeAndAfter = ({ before, after }) => { 7 | const [ currentWidth, setCurrentWidth] = useState(null) 8 | 9 | useEffect(() => { 10 | function handleResize() { 11 | const { innerWidth } = window; 12 | setCurrentWidth(innerWidth); 13 | } 14 | 15 | handleResize(); 16 | window.addEventListener('resize', handleResize); 17 | return () => window.removeEventListener('resize', handleResize); 18 | }, []) 19 | 20 | return ( 21 |
22 | 23 | {currentWidth && currentWidth >= 650 && ( 24 |
25 | )} 26 | {currentWidth && currentWidth < 650 && ( 27 |
28 | )} 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default BeforeAndAfter; 35 | -------------------------------------------------------------------------------- /docs/src/theme/BeforeAndAfter/styles.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | align-items: center; 6 | margin: 50px 0; 7 | } 8 | 9 | .gifs { 10 | border-radius: 5%; 11 | height: 500px 12 | } 13 | 14 | @media only screen and (max-width: 650px) { 15 | .container { 16 | flex-direction: column; 17 | } 18 | } 19 | 20 | .rightArrow { 21 | margin: 0 40px; 22 | font-size: 50px; 23 | } 24 | 25 | .downArrow { 26 | margin: 40px 0; 27 | font-size: 50px; 28 | } -------------------------------------------------------------------------------- /docs/src/theme/Footer/styles.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* 9 | SWM - commented out to remove opacity in component 10 | */ 11 | 12 | .footerLogoLink { 13 | opacity: 0.5; 14 | transition: opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default); 15 | } 16 | 17 | .footerLogoLink:hover { 18 | opacity: 1; 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/theme/TutorialStep/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | import styles from './styles.module.css'; 4 | import clsx from 'clsx'; 5 | 6 | const MARGIN_BOTTOM = 60; 7 | const TutorialStep = ({ children, title }) => { 8 | const [isActive, setIsActive] = useState(false); 9 | const componentRef = useRef(); 10 | const handleScroll = () => { 11 | const height = window.innerHeight; 12 | const position = window.pageYOffset; 13 | const minScroll = componentRef.current.offsetTop - height/3; 14 | const maxScroll = componentRef.current.offsetTop + componentRef.current.scrollHeight + MARGIN_BOTTOM - height/3; 15 | if (position > minScroll && position < maxScroll) { 16 | setIsActive(true); 17 | } else { 18 | setIsActive(false); 19 | } 20 | }; 21 | 22 | useEffect(() => { 23 | handleScroll(); 24 | window.addEventListener('scroll', handleScroll, { passive: true }); 25 | return () => { 26 | window.removeEventListener('scroll', handleScroll); 27 | }; 28 | }, []); 29 | 30 | return ( 31 |
32 |
33 |
34 |
{title}
35 | {children[0]} 36 |
37 |
38 |
{children[1]}
39 |
40 | ); 41 | }; 42 | 43 | export default TutorialStep; 44 | -------------------------------------------------------------------------------- /docs/src/theme/TutorialStep/styles.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | margin-bottom: 60px; 5 | } 6 | 7 | @media only screen and (max-width: 1200px) { 8 | .container { 9 | flex-direction: column; 10 | align-items: center; 11 | } 12 | 13 | .roundedStep { 14 | position: static !important;; 15 | } 16 | 17 | .description { 18 | justify-content: center; 19 | } 20 | } 21 | .description { 22 | flex: 1; 23 | display: flex; 24 | margin-bottom: 20px; 25 | height: fit-content; 26 | max-width: 450px; 27 | width: 100%; 28 | } 29 | 30 | .roundedStep { 31 | border-radius: 10px; 32 | height: fit-content; 33 | position: relative; 34 | top: 40px; 35 | padding: 10px; 36 | background-color: #dcdfeb; 37 | border-left: 6px solid #dcdfeb; 38 | font-size: 16px; 39 | line-height: 1.5; 40 | font-weight: 400; 41 | letter-spacing: -0.2px; 42 | margin-right: 20px; 43 | } 44 | 45 | 46 | .stepTitle { 47 | font-size: 12px; 48 | line-height: 1.33; 49 | font-weight: 600; 50 | letter-spacing: -0.1px; 51 | margin-bottom: 10px; 52 | } 53 | 54 | .code { 55 | flex: 1; 56 | display: flex; 57 | align-items: center; 58 | overflow: auto; 59 | max-width: 100%; 60 | } 61 | 62 | .code img, 63 | .code div { 64 | width: 100%; 65 | height: 100%; 66 | margin: 0 67 | } 68 | 69 | .codeInactive img, 70 | .codeInactive div { 71 | opacity: 0.5; 72 | } -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Air.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Air.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Air.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Air.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-AirItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-AirItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-AirItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-AirItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Black.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Black.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-BlackItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-BlackItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Bold.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Bold.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-BoldItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-BoldItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Light.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Light.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-LightItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-LightItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Medium.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Medium.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-MediumItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-MediumItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Regular.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Regular.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-RegularItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-RegularItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-RegularItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-RegularItalic.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Thin.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-Thin.woff2 -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-ThinItalic.woff -------------------------------------------------------------------------------- /docs/static/fonts/Aeonik-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/fonts/Aeonik-ThinItalic.woff2 -------------------------------------------------------------------------------- /docs/static/img/amazon-ddb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/img/amazon-ddb.png -------------------------------------------------------------------------------- /docs/static/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/img/banner.png -------------------------------------------------------------------------------- /docs/static/img/dynamode-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/img/dynamode-logo.png -------------------------------------------------------------------------------- /docs/static/img/fav_192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blazejkustra/dynamode/ab81ad1f412b928ffaad3162654c96fb026751c4/docs/static/img/fav_192x192.png -------------------------------------------------------------------------------- /docs/static/img/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /docs/static/img/swm-logo-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/batchDelete.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function batchDelete() { 4 | const modelBatchDelete = await AllPossiblePropertiesManager.batchDelete( 5 | [ 6 | { partitionKey: 'pk1', sortKey: 'sk1' }, 7 | { partitionKey: 'pk2', sortKey: 'sk2' }, 8 | ], 9 | { return: 'default' }, 10 | ); 11 | 12 | console.log(); 13 | console.log('OUTPUT:'); 14 | console.log(modelBatchDelete); 15 | } 16 | 17 | batchDelete(); 18 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/batchGet.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function batchGet() { 4 | const modelBatchGet = await AllPossiblePropertiesManager.batchGet([ 5 | { partitionKey: 'pk1', sortKey: 'sk1' }, 6 | { partitionKey: 'pk2', sortKey: 'sk2' }, 7 | ]); 8 | 9 | console.log(); 10 | console.log('OUTPUT:'); 11 | console.log(modelBatchGet); 12 | } 13 | 14 | batchGet(); 15 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/batchPut.ts: -------------------------------------------------------------------------------- 1 | import { AllPossibleProperties, AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function batchPut() { 4 | const modelBatchPut = await AllPossiblePropertiesManager.batchPut([ 5 | new AllPossibleProperties({ 6 | partitionKey: 'pk1', 7 | sortKey: 'sk1', 8 | string: 'kustra.blazej@gmail.com', 9 | object: { optional: 'test', required: 2 }, 10 | set: new Set('123'), 11 | array: ['1'], 12 | number: 10, 13 | map: new Map([['1', 'test']]), 14 | boolean: true, 15 | binary: new Uint8Array([1, 2, 3]), 16 | GSI_1_PK: 'test', 17 | GSI_1_SK: 1, 18 | }), 19 | new AllPossibleProperties({ 20 | partitionKey: 'pk2', 21 | sortKey: 'sk2', 22 | string: 'kustra.blazej@gmail.com', 23 | object: { optional: 'test', required: 2 }, 24 | set: new Set('123'), 25 | array: ['1'], 26 | number: 10, 27 | map: new Map([['1', 'test']]), 28 | boolean: true, 29 | binary: new Uint8Array([1, 2, 3]), 30 | GSI_1_PK: 'test', 31 | GSI_1_SK: 2, 32 | }), 33 | ]); 34 | 35 | console.log(); 36 | console.log('OUTPUT:'); 37 | console.log(modelBatchPut); 38 | } 39 | 40 | batchPut(); 41 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/create.ts: -------------------------------------------------------------------------------- 1 | import { AllPossibleProperties, AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function create() { 4 | const item = await AllPossiblePropertiesManager.create( 5 | new AllPossibleProperties({ 6 | partitionKey: 'pk1', 7 | sortKey: 'sk1', 8 | string: 'kustra.blazej@gmail.com', 9 | object: { optional: 'test', required: 2 }, 10 | set: new Set('123'), 11 | array: ['1'], 12 | number: 10, 13 | map: new Map([['1', 'test']]), 14 | boolean: true, 15 | binary: new Uint8Array([1, 2, 3]), 16 | }), 17 | ); 18 | 19 | console.log(); 20 | console.log('OUTPUT:'); 21 | console.log(item); 22 | } 23 | 24 | create(); 25 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/delete.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function deleteFn() { 4 | const modelDelete = await AllPossiblePropertiesManager.delete({ partitionKey: 'pk1', sortKey: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelDelete); 9 | } 10 | 11 | deleteFn(); 12 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/get.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function get() { 4 | const modelGet = await AllPossiblePropertiesManager.get({ partitionKey: 'pk1', sortKey: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelGet); 9 | } 10 | 11 | get(); 12 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/put.ts: -------------------------------------------------------------------------------- 1 | import { AllPossibleProperties, AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function put() { 4 | const item = await AllPossiblePropertiesManager.put( 5 | new AllPossibleProperties({ 6 | partitionKey: 'pk1', 7 | sortKey: 'sk1', 8 | string: 'kustra.blazej@gmail.com', 9 | LSI_1_SK: 105, 10 | object: { optional: 'test', required: 2 }, 11 | set: new Set('123'), 12 | array: ['1'], 13 | number: 10, 14 | map: new Map([['1', 'test']]), 15 | boolean: true, 16 | binary: new Uint8Array([1, 2, 3]), 17 | }), 18 | ); 19 | 20 | console.log(); 21 | console.log('OUTPUT:'); 22 | console.log(item); 23 | } 24 | 25 | put(); 26 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/query.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function query() { 4 | const userQuery = await AllPossiblePropertiesManager.query() 5 | .partitionKey('partitionKey') 6 | .eq('pk1') 7 | .condition( 8 | AllPossiblePropertiesManager.condition() 9 | .attribute('number') 10 | .le(2) 11 | .or.parenthesis( 12 | AllPossiblePropertiesManager.condition() 13 | .attribute('GSI_1_PK') 14 | .not() 15 | .eq('user') 16 | .and.attribute('string') 17 | .beginsWith('kustra.blazej'), 18 | ), 19 | ) 20 | .run(); 21 | 22 | console.log(); 23 | console.log('OUTPUT:'); 24 | console.log(userQuery); 25 | } 26 | 27 | query(); 28 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/scan.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function scan() { 4 | const scan1 = await AllPossiblePropertiesManager.scan() 5 | .attribute('string') 6 | .beginsWith('k') 7 | .indexName('GSI_1_NAME') 8 | .limit(1) 9 | .run(); 10 | 11 | const scan2 = await AllPossiblePropertiesManager.scan() 12 | .attribute('string') 13 | .beginsWith('k') 14 | .startAt(scan1.lastKey) 15 | .indexName('GSI_1_NAME') 16 | .run(); 17 | 18 | console.log(); 19 | console.log('OUTPUT:'); 20 | console.log(scan1); 21 | console.log(scan2); 22 | } 23 | 24 | scan(); 25 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/transactionGet.ts: -------------------------------------------------------------------------------- 1 | import transactionGet from '../../../dist/transactionGet'; 2 | import { AllPossiblePropertiesManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionGet([ 6 | AllPossiblePropertiesManager.transaction.get({ 7 | partitionKey: 'pk1', 8 | sortKey: 'sk1', 9 | }), 10 | AllPossiblePropertiesManager.transaction.get({ 11 | partitionKey: 'pk2', 12 | sortKey: 'sk2', 13 | }), 14 | ]); 15 | 16 | console.log(); 17 | console.log('OUTPUT:'); 18 | console.log(transactions); 19 | } 20 | 21 | transaction(); 22 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/transactionWrite.ts: -------------------------------------------------------------------------------- 1 | import transactionWrite from '../../../dist/transactionWrite'; 2 | import { AllPossibleProperties, AllPossiblePropertiesManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionWrite( 6 | [ 7 | AllPossiblePropertiesManager.transaction.update( 8 | { partitionKey: 'pk1', sortKey: 'sk1' }, 9 | { 10 | add: { 11 | set: new Set(['5']), 12 | }, 13 | }, 14 | { 15 | condition: AllPossiblePropertiesManager.condition().attribute('partitionKey').eq('pk1'), 16 | }, 17 | ), 18 | AllPossiblePropertiesManager.transaction.put( 19 | new AllPossibleProperties({ 20 | partitionKey: 'pk2', 21 | sortKey: 'sk2', 22 | string: 'kustra.blazej@gmail.com', 23 | LSI_1_SK: 105, 24 | object: { optional: 'test', required: 2 }, 25 | set: new Set('123'), 26 | array: ['1'], 27 | number: 10, 28 | map: new Map([['1', 'test']]), 29 | boolean: true, 30 | binary: new Uint8Array([1, 2, 3]), 31 | }), 32 | ), 33 | AllPossiblePropertiesManager.transaction.create( 34 | new AllPossibleProperties({ 35 | partitionKey: 'pk3', 36 | sortKey: 'sk3', 37 | string: 'kustra.blazej@gmail.com', 38 | LSI_1_SK: 105, 39 | object: { optional: 'test', required: 2 }, 40 | set: new Set('123'), 41 | array: ['1'], 42 | number: 10, 43 | map: new Map([['1', 'test']]), 44 | boolean: true, 45 | binary: new Uint8Array([1, 2, 3]), 46 | }), 47 | ), 48 | AllPossiblePropertiesManager.transaction.delete({ 49 | partitionKey: 'pk2', 50 | sortKey: 'sk2', 51 | }), 52 | AllPossiblePropertiesManager.transaction.condition( 53 | { partitionKey: 'pk1', sortKey: 'sk1' }, 54 | AllPossiblePropertiesManager.condition().attribute('partitionKey').eq('pk1'), 55 | ), 56 | ], 57 | { return: 'default' }, 58 | ); 59 | 60 | console.log(); 61 | console.log('OUTPUT:'); 62 | console.log(transactions); 63 | } 64 | 65 | transaction(); 66 | -------------------------------------------------------------------------------- /examples/AllPossibleProperties/methods/update.ts: -------------------------------------------------------------------------------- 1 | import { AllPossiblePropertiesManager } from '../model'; 2 | 3 | async function update() { 4 | const userUpdate = await AllPossiblePropertiesManager.update( 5 | { partitionKey: 'pk1', sortKey: 'sk1' }, 6 | { 7 | add: { 8 | set: new Set(['5']), 9 | }, 10 | // set: { 11 | // string: 'string', 12 | // }, 13 | // setIfNotExists: { 14 | // 'object.optional': 'optional', 15 | // }, 16 | // listAppend: { 17 | // array: ['value'], 18 | // }, 19 | // increment: { 20 | // number: 10, 21 | // }, 22 | // decrement: { 23 | // 'object.required': 2, 24 | // }, 25 | // delete: { 26 | // set: new Set(['2', '5']), 27 | // }, 28 | // remove: ['object.optional'], 29 | }, 30 | ); 31 | 32 | console.log(); 33 | console.log('OUTPUT:'); 34 | console.log(userUpdate); 35 | } 36 | 37 | update(); 38 | -------------------------------------------------------------------------------- /examples/Inheritance/methods/get.ts: -------------------------------------------------------------------------------- 1 | import { EntityOneManager, EntityThreeManager, EntityTwoManager } from '../model'; 2 | 3 | async function get() { 4 | const model1Get = await EntityOneManager.get({ propPk: 'propPk', propSk: 101 }); 5 | const model2Get = await EntityTwoManager.get({ propPk: 'propPk', propSk: 102 }); 6 | const model3Get = await EntityThreeManager.get({ propPk: 'propPk', propSk: 103 }); 7 | 8 | console.log(); 9 | console.log('OUTPUT:'); 10 | console.log(model1Get); 11 | console.log(model2Get); 12 | console.log(model3Get); 13 | } 14 | 15 | get(); 16 | -------------------------------------------------------------------------------- /examples/Inheritance/methods/put.ts: -------------------------------------------------------------------------------- 1 | import { EntityOne, EntityOneManager, EntityThree, EntityThreeManager, EntityTwo, EntityTwoManager } from '../model'; 2 | 3 | async function put() { 4 | const item1 = await EntityOneManager.put( 5 | new EntityOne({ 6 | propPk: 'propPk', 7 | propSk: 101, 8 | index: 'index', 9 | one: { test: 2 }, 10 | }), 11 | ); 12 | const item2 = await EntityTwoManager.put( 13 | new EntityTwo({ 14 | propPk: 'propPk', 15 | propSk: 102, 16 | index: 'index', 17 | one: { test: 2 }, 18 | two: { test: '2' }, 19 | }), 20 | ); 21 | const item3 = await EntityThreeManager.put( 22 | new EntityThree({ 23 | propPk: 'propPk', 24 | propSk: 103, 25 | index: 'index', 26 | otherProperty: { test: 2 }, 27 | }), 28 | ); 29 | 30 | console.log(); 31 | console.log('OUTPUT:'); 32 | console.log(item1); 33 | console.log(item2); 34 | console.log(item3); 35 | } 36 | 37 | put(); 38 | -------------------------------------------------------------------------------- /examples/Inheritance/model.ts: -------------------------------------------------------------------------------- 1 | import attribute from '../../dist/decorators'; 2 | import Dynamode from '../../dist/dynamode'; 3 | import Entity from '../../dist/entity'; 4 | import TableManager from '../../dist/table'; 5 | 6 | Dynamode.ddb.local(); 7 | 8 | type TableProps = { 9 | propPk: string; 10 | propSk: number; 11 | index: string; 12 | }; 13 | 14 | const TABLE_NAME = 'inheritance'; 15 | 16 | class BaseTable extends Entity { 17 | @attribute.partitionKey.string() 18 | propPk: string; 19 | 20 | @attribute.sortKey.number() 21 | propSk: number; 22 | 23 | @attribute.lsi.sortKey.string({ indexName: 'LSI_NAME' }) 24 | index: string; 25 | 26 | constructor(props: TableProps) { 27 | super(); 28 | 29 | this.propPk = props.propPk; 30 | this.propSk = props.propSk; 31 | this.index = props.index; 32 | } 33 | } 34 | 35 | type EntityOneProps = TableProps & { 36 | one: { [k: string]: number }; 37 | }; 38 | 39 | export class EntityOne extends BaseTable { 40 | @attribute.object() 41 | one: { [k: string]: number }; 42 | 43 | constructor(props: EntityOneProps) { 44 | super(props); 45 | 46 | this.one = props.one; 47 | } 48 | } 49 | 50 | type EntityTwoProps = EntityOneProps & { 51 | two: { [k: string]: string }; 52 | }; 53 | 54 | export class EntityTwo extends EntityOne { 55 | @attribute.object() 56 | two: { [k: string]: string }; 57 | 58 | constructor(props: EntityTwoProps) { 59 | super(props); 60 | 61 | this.two = props.two; 62 | } 63 | } 64 | 65 | type EntityThreeProps = TableProps & { 66 | otherProperty: any; 67 | }; 68 | 69 | export class EntityThree extends BaseTable { 70 | @attribute.object() 71 | otherProperty: { [k: string]: number }; 72 | 73 | constructor(props: EntityThreeProps) { 74 | super(props); 75 | 76 | this.otherProperty = props.otherProperty; 77 | } 78 | } 79 | 80 | export const BaseTableManager = new TableManager(BaseTable, { 81 | tableName: TABLE_NAME, 82 | partitionKey: 'propPk', 83 | sortKey: 'propSk', 84 | indexes: { 85 | LSI_NAME: { 86 | sortKey: 'index', 87 | }, 88 | }, 89 | }); 90 | 91 | export const EntityOneManager = BaseTableManager.entityManager(EntityOne); 92 | export const EntityTwoManager = BaseTableManager.entityManager(EntityTwo); 93 | export const EntityThreeManager = BaseTableManager.entityManager(EntityThree); 94 | 95 | async function create() { 96 | const table = await BaseTableManager.createTable(); 97 | console.log(table); 98 | } 99 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/batchDelete.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function batchDelete() { 4 | const modelBatchDelete = await KeyValueManager.batchDelete([{ key: 'key1' }, { key: 'key2' }], { return: 'default' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelBatchDelete); 9 | } 10 | 11 | batchDelete(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/batchGet.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function batchGet() { 4 | const modelBatchGet = await KeyValueManager.batchGet([{ key: 'key1' }, { key: 'key2' }]); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelBatchGet); 9 | } 10 | 11 | batchGet(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/batchPut.ts: -------------------------------------------------------------------------------- 1 | import { KeyValue, KeyValueManager } from '../model'; 2 | 3 | async function batchPut() { 4 | const modelBatchPut = await KeyValueManager.batchPut([ 5 | new KeyValue({ 6 | key: 'key1', 7 | value: { test: 123 }, 8 | }), 9 | new KeyValue({ 10 | key: 'key2', 11 | value: { test: 123 }, 12 | }), 13 | ]); 14 | 15 | console.log(); 16 | console.log('OUTPUT:'); 17 | console.log(modelBatchPut); 18 | } 19 | 20 | batchPut(); 21 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/create.ts: -------------------------------------------------------------------------------- 1 | import { KeyValue, KeyValueManager } from '../model'; 2 | 3 | async function create() { 4 | const item = await KeyValueManager.create( 5 | new KeyValue({ 6 | key: 'key1', 7 | value: { test: 123 }, 8 | }), 9 | ); 10 | 11 | console.log(); 12 | console.log('OUTPUT:'); 13 | console.log(item); 14 | } 15 | 16 | create(); 17 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/delete.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function deleteFn() { 4 | const modelDelete = await KeyValueManager.delete({ key: 'key1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelDelete); 9 | } 10 | 11 | deleteFn(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/get.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function get() { 4 | const modelGet = await KeyValueManager.get({ key: 'key1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(modelGet); 9 | } 10 | 11 | get(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/put.ts: -------------------------------------------------------------------------------- 1 | import { KeyValue, KeyValueManager } from '../model'; 2 | 3 | async function put() { 4 | const item = await KeyValueManager.put( 5 | new KeyValue({ 6 | key: 'key1', 7 | value: { test: 123 }, 8 | }), 9 | ); 10 | 11 | console.log(); 12 | console.log('OUTPUT:'); 13 | console.log(item); 14 | } 15 | 16 | put(); 17 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/query.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function query() { 4 | const userQuery = await KeyValueManager.query().partitionKey('key').eq('key1').run(); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(userQuery); 9 | } 10 | 11 | query(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/scan.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function scan() { 4 | const userScan = await KeyValueManager.scan().attribute('key').beginsWith('k').run(); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(userScan); 9 | } 10 | 11 | scan(); 12 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/transactionGet.ts: -------------------------------------------------------------------------------- 1 | import transactionGet from '../../../dist/transactionGet'; 2 | import { KeyValueManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionGet([ 6 | KeyValueManager.transaction.get({ key: 'key1' }), 7 | KeyValueManager.transaction.get({ key: 'key2' }), 8 | ]); 9 | 10 | console.log(); 11 | console.log('OUTPUT:'); 12 | console.log(transactions); 13 | } 14 | 15 | transaction(); 16 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/transactionWrite.ts: -------------------------------------------------------------------------------- 1 | import transactionWrite from '../../../dist/transactionWrite'; 2 | import { KeyValue, KeyValueManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionWrite( 6 | [ 7 | KeyValueManager.transaction.update( 8 | { key: 'key1' }, 9 | { 10 | set: { 11 | value: { test: 'test' }, 12 | }, 13 | increment: {}, 14 | }, 15 | ), 16 | KeyValueManager.transaction.put( 17 | new KeyValue({ 18 | key: 'key2', 19 | value: { test: '' }, 20 | }), 21 | ), 22 | KeyValueManager.transaction.create( 23 | new KeyValue({ 24 | key: 'key3', 25 | value: { test: '' }, 26 | }), 27 | ), 28 | KeyValueManager.transaction.delete({ key: 'key4' }), 29 | KeyValueManager.transaction.condition({ key: 'key5' }, KeyValueManager.condition().attribute('key').eq('key5')), 30 | ], 31 | { return: 'default' }, 32 | ); 33 | 34 | console.log(); 35 | console.log('OUTPUT:'); 36 | console.log(transactions); 37 | } 38 | 39 | transaction(); 40 | -------------------------------------------------------------------------------- /examples/KeyValue/methods/update.ts: -------------------------------------------------------------------------------- 1 | import { KeyValueManager } from '../model'; 2 | 3 | async function update() { 4 | const userUpdate = await KeyValueManager.update( 5 | { key: 'key1' }, 6 | { 7 | set: { 8 | 'value.lol': 2, 9 | }, 10 | remove: [], 11 | }, 12 | ); 13 | 14 | console.log(); 15 | console.log('OUTPUT:'); 16 | console.log(userUpdate); 17 | } 18 | 19 | update(); 20 | -------------------------------------------------------------------------------- /examples/KeyValue/model.ts: -------------------------------------------------------------------------------- 1 | import attribute from '../../dist/decorators'; 2 | import Dynamode from '../../dist/dynamode'; 3 | import Entity from '../../dist/entity'; 4 | import TableManager from '../../dist/table'; 5 | 6 | Dynamode.ddb.local(); 7 | 8 | type KeyValueProps = { 9 | key: string; 10 | value: Record; 11 | }; 12 | 13 | const TABLE_NAME = 'key-value'; 14 | 15 | export class KeyValue extends Entity { 16 | @attribute.partitionKey.string() 17 | key: string; 18 | 19 | @attribute.object() 20 | value: Record; 21 | 22 | constructor(props: KeyValueProps) { 23 | super(); 24 | 25 | this.key = props.key; 26 | this.value = props.value; 27 | } 28 | } 29 | 30 | export const KeyValueTableManager = new TableManager(KeyValue, { 31 | tableName: TABLE_NAME, 32 | partitionKey: 'key', 33 | }); 34 | 35 | export const KeyValueManager = KeyValueTableManager.entityManager(); 36 | 37 | async function create() { 38 | const table = await KeyValueTableManager.createTable({ 39 | tags: { 40 | 'dynamode:example': 'key-value', 41 | }, 42 | throughput: { 43 | read: 1, 44 | write: 1, 45 | }, 46 | deletionProtection: true, 47 | }); 48 | console.log(table); 49 | } 50 | 51 | async function validateTable() { 52 | const table = await KeyValueTableManager.validateTable(); 53 | console.log(table); 54 | } 55 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/batchDelete.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function batchDelete() { 4 | const entityReservedWordBatchDelete = await ReservedWordManager.batchDelete([ 5 | { COLUMN: 'pk1', OBJECT: 'sk1' }, 6 | { COLUMN: 'pk2', OBJECT: 'sk2' }, 7 | ]); 8 | 9 | console.log(); 10 | console.log('OUTPUT:'); 11 | console.log(entityReservedWordBatchDelete); 12 | } 13 | 14 | batchDelete(); 15 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/batchGet.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function batchGet() { 4 | const entityReservedWordBatchGet = await ReservedWordManager.batchGet([ 5 | { COLUMN: 'pk1', OBJECT: 'sk1' }, 6 | { COLUMN: 'pk2', OBJECT: 'sk2' }, 7 | ]); 8 | 9 | console.log(); 10 | console.log('OUTPUT:'); 11 | console.log(entityReservedWordBatchGet); 12 | } 13 | 14 | batchGet(); 15 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/batchPut.ts: -------------------------------------------------------------------------------- 1 | import { EntityReservedWord, ReservedWordManager } from '../model'; 2 | 3 | async function batchPut() { 4 | const entityReservedWordBatchPut = await ReservedWordManager.batchPut([ 5 | new EntityReservedWord({ 6 | COLUMN: 'pk1', 7 | OBJECT: 'sk1', 8 | COPY: 'copy', 9 | DEFAULT: 105, 10 | old: 105, 11 | }), 12 | new EntityReservedWord({ 13 | COLUMN: 'pk2', 14 | OBJECT: 'sk2', 15 | COPY: 'copy', 16 | DEFAULT: 105, 17 | old: 105, 18 | }), 19 | ]); 20 | 21 | console.log(); 22 | console.log('OUTPUT:'); 23 | console.log(entityReservedWordBatchPut); 24 | } 25 | 26 | batchPut(); 27 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/create.ts: -------------------------------------------------------------------------------- 1 | import { EntityReservedWord, ReservedWordManager } from '../model'; 2 | 3 | async function create() { 4 | const entityReservedWord = await ReservedWordManager.create( 5 | new EntityReservedWord({ 6 | COLUMN: 'pk1', 7 | OBJECT: 'sk1', 8 | COPY: 'copy', 9 | DEFAULT: 105, 10 | old: 105, 11 | }), 12 | ); 13 | 14 | console.log(); 15 | console.log('OUTPUT:'); 16 | console.log(entityReservedWord); 17 | } 18 | 19 | create(); 20 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/delete.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function deleteFn() { 4 | const entityReservedWordDelete = await ReservedWordManager.delete({ COLUMN: 'pk1', OBJECT: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(entityReservedWordDelete); 9 | } 10 | 11 | deleteFn(); 12 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/get.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function get() { 4 | const userGet = await ReservedWordManager.get({ COLUMN: 'pk1', OBJECT: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(userGet); 9 | } 10 | 11 | get(); 12 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/put.ts: -------------------------------------------------------------------------------- 1 | import { EntityReservedWord, ReservedWordManager } from '../model'; 2 | 3 | async function put() { 4 | const entityReservedWord = await ReservedWordManager.put( 5 | new EntityReservedWord({ 6 | COLUMN: 'pk3', 7 | OBJECT: 'sk3', 8 | COPY: 'copy', 9 | DEFAULT: 105, 10 | old: 105, 11 | }), 12 | ); 13 | 14 | console.log(); 15 | console.log('OUTPUT:'); 16 | console.log(entityReservedWord); 17 | } 18 | 19 | put(); 20 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/query.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function query() { 4 | const entityReservedWordQuery = await ReservedWordManager.query() 5 | .partitionKey('COLUMN') 6 | .eq('pk1') 7 | .attributes(['old']) 8 | .run({ return: 'input' }); 9 | 10 | console.log(); 11 | console.log('OUTPUT:'); 12 | console.log(entityReservedWordQuery); 13 | } 14 | 15 | query(); 16 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/scan.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function scan() { 4 | const entityReservedWordScan = await ReservedWordManager.scan() 5 | .attribute('COLUMN') 6 | .beginsWith('pk') 7 | .startAt({ OBJECT: 'sk1', COLUMN: 'pk1' }) 8 | .limit(1) 9 | .run(); 10 | 11 | console.log(); 12 | console.log('OUTPUT:'); 13 | console.log(entityReservedWordScan); 14 | } 15 | 16 | scan(); 17 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/transactionGet.ts: -------------------------------------------------------------------------------- 1 | import transactionGet from '../../../dist/transactionGet'; 2 | import { ReservedWordManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionGet([ 6 | ReservedWordManager.transaction.get({ COLUMN: 'pk1', OBJECT: 'sk1' }), 7 | ReservedWordManager.transaction.get({ COLUMN: 'pk2', OBJECT: 'sk2' }), 8 | ]); 9 | 10 | console.log(); 11 | console.log('OUTPUT:'); 12 | console.log(transactions); 13 | } 14 | 15 | transaction(); 16 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/transactionWrite.ts: -------------------------------------------------------------------------------- 1 | import transactionWrite from '../../../dist/transactionWrite'; 2 | import { EntityReservedWord, ReservedWordManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionWrite( 6 | [ 7 | ReservedWordManager.transaction.update( 8 | { COLUMN: 'pk1', OBJECT: 'sk1' }, 9 | { 10 | add: { 11 | DEFAULT: 10, 12 | }, 13 | }, 14 | ), 15 | ReservedWordManager.transaction.put( 16 | new EntityReservedWord({ 17 | COLUMN: 'pk2', 18 | OBJECT: 'sk2', 19 | COPY: 'copy', 20 | DEFAULT: 105, 21 | old: 105, 22 | }), 23 | ), 24 | ReservedWordManager.transaction.create( 25 | new EntityReservedWord({ 26 | COLUMN: 'pk3', 27 | OBJECT: 'sk3', 28 | COPY: 'copy', 29 | DEFAULT: 105, 30 | old: 105, 31 | }), 32 | ), 33 | ReservedWordManager.transaction.delete({ COLUMN: 'pk5', OBJECT: 'sk5' }), 34 | ReservedWordManager.transaction.condition( 35 | { COLUMN: 'pk6', OBJECT: 'sk6' }, 36 | ReservedWordManager.condition().attribute('COLUMN').not().exists(), 37 | ), 38 | ], 39 | { return: 'default' }, 40 | ); 41 | 42 | console.log(); 43 | console.log('OUTPUT:'); 44 | console.log(transactions); 45 | } 46 | 47 | transaction(); 48 | -------------------------------------------------------------------------------- /examples/ReservedWord/methods/update.ts: -------------------------------------------------------------------------------- 1 | import { ReservedWordManager } from '../model'; 2 | 3 | async function update() { 4 | const entityReservedWordUpdate = await ReservedWordManager.update( 5 | { COLUMN: 'pk1', OBJECT: 'sk1' }, 6 | { 7 | add: { 8 | DEFAULT: 10, 9 | }, 10 | }, 11 | ); 12 | 13 | console.log(); 14 | console.log('OUTPUT:'); 15 | console.log(entityReservedWordUpdate); 16 | } 17 | 18 | update(); 19 | -------------------------------------------------------------------------------- /examples/ReservedWord/model.ts: -------------------------------------------------------------------------------- 1 | import attribute from '../../dist/decorators'; 2 | import Dynamode from '../../dist/dynamode'; 3 | import Entity from '../../dist/entity'; 4 | import TableManager from '../../dist/table'; 5 | 6 | Dynamode.ddb.local(); 7 | 8 | type ReservedWordProps = { 9 | COLUMN: string; 10 | OBJECT: string; 11 | COPY: string; 12 | DEFAULT?: number; 13 | old?: number; 14 | DAY?: Date; 15 | DATE?: Date; 16 | }; 17 | 18 | const TABLE_NAME = 'reservedWord'; 19 | 20 | export class EntityReservedWord extends Entity { 21 | // Primary key 22 | @attribute.partitionKey.string() 23 | COLUMN: string; 24 | 25 | @attribute.sortKey.string() 26 | OBJECT: string; 27 | 28 | // Indexes 29 | @attribute.gsi.partitionKey.string({ indexName: 'OTHER' }) 30 | COPY?: string; 31 | 32 | @attribute.gsi.sortKey.number({ indexName: 'OTHER' }) 33 | DEFAULT?: number; 34 | 35 | @attribute.lsi.sortKey.number({ indexName: 'PRIMARY' }) 36 | old?: number; 37 | 38 | // Dates 39 | @attribute.date.string() 40 | DAY: Date; 41 | 42 | @attribute.date.number() 43 | DATE: Date; 44 | 45 | constructor(props: ReservedWordProps) { 46 | super(); 47 | // Primary key 48 | this.COLUMN = props.COLUMN; 49 | this.OBJECT = props.OBJECT; 50 | 51 | // Indexes 52 | this.COPY = props.COPY; 53 | this.DEFAULT = props.DEFAULT; 54 | this.old = props.old; 55 | 56 | // Dates 57 | this.DAY = props.DAY || new Date(); 58 | this.DATE = props.DATE || new Date(); 59 | } 60 | } 61 | 62 | export const ReservedWordTableManager = new TableManager(EntityReservedWord, { 63 | tableName: TABLE_NAME, 64 | partitionKey: 'COLUMN', 65 | sortKey: 'OBJECT', 66 | indexes: { 67 | OTHER: { 68 | partitionKey: 'COPY', 69 | sortKey: 'DEFAULT', 70 | }, 71 | PRIMARY: { 72 | sortKey: 'old', 73 | }, 74 | }, 75 | }); 76 | 77 | export const ReservedWordManager = ReservedWordTableManager.entityManager(); 78 | -------------------------------------------------------------------------------- /examples/User/methods/batchDelete.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function batchDelete() { 4 | const userBatchDelete = await UserManager.batchDelete( 5 | [ 6 | { partitionKey: 'pk1', sortKey: 'sk1' }, 7 | { partitionKey: 'pk2', sortKey: 'sk2' }, 8 | ], 9 | { return: 'default' }, 10 | ); 11 | 12 | console.log(); 13 | console.log('OUTPUT:'); 14 | console.log(userBatchDelete); 15 | } 16 | 17 | batchDelete(); 18 | -------------------------------------------------------------------------------- /examples/User/methods/batchGet.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function batchGet() { 4 | const userBatchGet = await UserManager.batchGet([ 5 | { partitionKey: 'pk1', sortKey: 'sk1' }, 6 | { partitionKey: 'pk2', sortKey: 'sk2' }, 7 | ]); 8 | 9 | console.log(); 10 | console.log('OUTPUT:'); 11 | console.log(userBatchGet); 12 | } 13 | 14 | batchGet(); 15 | -------------------------------------------------------------------------------- /examples/User/methods/batchPut.ts: -------------------------------------------------------------------------------- 1 | import { User, UserManager } from '../model'; 2 | 3 | async function batchPut() { 4 | const userBatchPut = await UserManager.batchPut([ 5 | new User({ 6 | partitionKey: 'pk1', 7 | sortKey: 'sk1', 8 | username: 'blazej', 9 | email: 'blazej@gmail.com', 10 | age: 18, 11 | friends: ['tomas', 'david'], 12 | config: { 13 | isAdmin: true, 14 | }, 15 | }), 16 | new User({ 17 | partitionKey: 'pk2', 18 | sortKey: 'sk2', 19 | username: 'blazej', 20 | email: 'blazej@gmail.com', 21 | age: 18, 22 | friends: ['tomas', 'david'], 23 | config: { 24 | isAdmin: true, 25 | }, 26 | }), 27 | ]); 28 | 29 | console.log(); 30 | console.log('OUTPUT:'); 31 | console.log(userBatchPut); 32 | } 33 | 34 | batchPut(); 35 | -------------------------------------------------------------------------------- /examples/User/methods/create.ts: -------------------------------------------------------------------------------- 1 | import { User, UserManager } from '../model'; 2 | 3 | async function create() { 4 | const user = await UserManager.create( 5 | new User({ 6 | partitionKey: 'pk1', 7 | sortKey: 'sk1', 8 | username: 'blazej', 9 | email: 'blazej@gmail.com', 10 | age: 18, 11 | friends: ['tomas', 'david'], 12 | config: { 13 | isAdmin: true, 14 | }, 15 | }), 16 | ); 17 | 18 | console.log(); 19 | console.log('OUTPUT:'); 20 | console.log(user); 21 | } 22 | 23 | create(); 24 | -------------------------------------------------------------------------------- /examples/User/methods/delete.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function deleteFn() { 4 | const userDelete = await UserManager.delete({ partitionKey: 'pk1', sortKey: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(userDelete); 9 | } 10 | 11 | deleteFn(); 12 | -------------------------------------------------------------------------------- /examples/User/methods/get.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function get() { 4 | const userGet = await UserManager.get({ partitionKey: 'pk1', sortKey: 'sk1' }); 5 | 6 | console.log(); 7 | console.log('OUTPUT:'); 8 | console.log(userGet); 9 | } 10 | 11 | get(); 12 | -------------------------------------------------------------------------------- /examples/User/methods/put.ts: -------------------------------------------------------------------------------- 1 | import { User, UserManager } from '../model'; 2 | 3 | async function put() { 4 | const user = await UserManager.put( 5 | new User({ 6 | partitionKey: '1', 7 | sortKey: 'blazej', 8 | username: 'blazej', 9 | email: 'blazej@gmail.com', 10 | age: 18, 11 | friends: ['tomas', 'david'], 12 | config: { 13 | isAdmin: true, 14 | }, 15 | }), 16 | ); 17 | 18 | console.log(); 19 | console.log('OUTPUT:'); 20 | console.log(user); 21 | } 22 | 23 | put(); 24 | -------------------------------------------------------------------------------- /examples/User/methods/query.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function query() { 4 | const result = await UserManager.query() 5 | .partitionKey('partitionKey') 6 | .eq('1') 7 | .sortKey('sortKey') 8 | .beginsWith('bla') 9 | .limit(1) 10 | .sort('descending') 11 | .run({ return: 'output' }); 12 | 13 | console.log(); 14 | console.log('OUTPUT:'); 15 | console.log(result); 16 | } 17 | 18 | query(); 19 | -------------------------------------------------------------------------------- /examples/User/methods/scan.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function scan() { 4 | const result = await UserManager.scan() 5 | .attribute('age') 6 | .eq(18) 7 | .attribute('partitionKey') 8 | .beginsWith('1') 9 | .limit(3) 10 | .run({ return: 'output' }); 11 | 12 | console.log(); 13 | console.log('OUTPUT:'); 14 | console.log(result); 15 | } 16 | 17 | scan(); 18 | -------------------------------------------------------------------------------- /examples/User/methods/transactionGet.ts: -------------------------------------------------------------------------------- 1 | import transactionGet from '../../../dist/transactionGet'; 2 | import { UserManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionGet([ 6 | UserManager.transaction.get({ partitionKey: 'pk1', sortKey: 'sk1' }), 7 | UserManager.transaction.get({ partitionKey: 'pk1', sortKey: 'sk1' }), 8 | ]); 9 | 10 | console.log(); 11 | console.log('OUTPUT:'); 12 | console.log(transactions); 13 | } 14 | 15 | transaction(); 16 | -------------------------------------------------------------------------------- /examples/User/methods/transactionWrite.ts: -------------------------------------------------------------------------------- 1 | import transactionWrite from '../../../dist/transactionWrite'; 2 | import { User, UserManager } from '../model'; 3 | 4 | async function transaction() { 5 | const transactions = await transactionWrite([ 6 | UserManager.transaction.update( 7 | { partitionKey: 'pk1', sortKey: 'sk1' }, 8 | { 9 | set: { 10 | age: 18, 11 | }, 12 | }, 13 | { 14 | condition: UserManager.condition().attribute('partitionKey').eq('pk1'), 15 | }, 16 | ), 17 | UserManager.transaction.put( 18 | new User({ 19 | partitionKey: 'pk2', 20 | sortKey: 'sk2', 21 | username: 'blazej', 22 | email: 'blazej@gmail.com', 23 | age: 18, 24 | friends: ['tomas', 'david'], 25 | config: { 26 | isAdmin: true, 27 | }, 28 | }), 29 | ), 30 | UserManager.transaction.create( 31 | new User({ 32 | partitionKey: 'pk3', 33 | sortKey: 'sk3', 34 | username: 'blazej', 35 | email: 'blazej@gmail.com', 36 | age: 18, 37 | friends: ['tomas', 'david'], 38 | config: { 39 | isAdmin: true, 40 | }, 41 | }), 42 | ), 43 | UserManager.transaction.delete({ partitionKey: 'pk4', sortKey: 'sk4' }), 44 | UserManager.transaction.condition( 45 | { partitionKey: 'pk5', sortKey: 'sk5' }, 46 | UserManager.condition().attribute('partitionKey').eq('pk5'), 47 | ), 48 | ]); 49 | 50 | console.log(); 51 | console.log('OUTPUT:'); 52 | console.log(transactions); 53 | } 54 | 55 | transaction(); 56 | -------------------------------------------------------------------------------- /examples/User/methods/update.ts: -------------------------------------------------------------------------------- 1 | import { UserManager } from '../model'; 2 | 3 | async function update() { 4 | const userUpdate = await UserManager.update( 5 | { partitionKey: 'pk1', sortKey: 'sk1' }, 6 | { 7 | set: { 8 | age: 18, 9 | }, 10 | add: {}, 11 | }, 12 | ); 13 | 14 | console.log(); 15 | console.log('OUTPUT:'); 16 | console.log(userUpdate); 17 | } 18 | 19 | update(); 20 | -------------------------------------------------------------------------------- /examples/User/model.ts: -------------------------------------------------------------------------------- 1 | import attribute from '../../dist/decorators'; 2 | import Dynamode from '../../dist/dynamode'; 3 | import Entity from '../../dist/entity'; 4 | import TableManager from '../../dist/table'; 5 | 6 | Dynamode.ddb.local(); 7 | 8 | type UserProps = { 9 | partitionKey: string; 10 | sortKey: string; 11 | username: string; 12 | email: string; 13 | age: number; 14 | friends: string[]; 15 | config: { 16 | isAdmin: boolean; 17 | }; 18 | }; 19 | 20 | const USERS_TABLE = 'users'; 21 | 22 | export class User extends Entity { 23 | @attribute.partitionKey.string() 24 | partitionKey: string; 25 | 26 | @attribute.sortKey.string() 27 | sortKey: string; 28 | 29 | @attribute.string() 30 | username: string; 31 | 32 | @attribute.string() 33 | email: string; 34 | 35 | @attribute.number() 36 | age: number; 37 | 38 | @attribute.array() 39 | friends: string[]; 40 | 41 | @attribute.object() 42 | config: { 43 | isAdmin: boolean; 44 | }; 45 | 46 | constructor(props: UserProps) { 47 | super(); 48 | 49 | // Primary key 50 | this.partitionKey = props.partitionKey; 51 | this.sortKey = props.sortKey; 52 | 53 | // Other properties 54 | this.username = props.username; 55 | this.email = props.email; 56 | this.age = props.age; 57 | this.friends = props.friends; 58 | this.config = props.config; 59 | } 60 | } 61 | 62 | export const UserTableManager = new TableManager(User, { 63 | tableName: USERS_TABLE, 64 | partitionKey: 'partitionKey', 65 | sortKey: 'sortKey', 66 | }); 67 | 68 | export const UserManager = UserTableManager.entityManager(); 69 | 70 | async function create() { 71 | const table = await UserTableManager.createTable(); 72 | console.log(table); 73 | } 74 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | dynamodb-local: 4 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" 5 | image: "amazon/dynamodb-local:latest" 6 | container_name: dynamodb-local 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - "./docker/dynamodb:/home/dynamodblocal/data" 11 | working_dir: /home/dynamodblocal -------------------------------------------------------------------------------- /examples/playground.ts: -------------------------------------------------------------------------------- 1 | import attribute from '../dist/decorators'; 2 | import Entity from '../dist/entity'; 3 | import TableManager from '../dist/table'; 4 | 5 | export class TableUser extends Entity { 6 | @attribute.gsi.partitionKey.string({ indexName: 'index-1' }) 7 | @attribute.partitionKey.string() 8 | domain: string; 9 | 10 | @attribute.sortKey.string() 11 | @attribute.gsi.sortKey.string({ indexName: 'index-1' }) 12 | email: string; 13 | 14 | constructor(data: { domain: string; email: string }) { 15 | super(data); 16 | this.domain = data.domain; 17 | this.email = data.email; 18 | } 19 | } 20 | 21 | export const UserdataTableManager = new TableManager(TableUser, { 22 | tableName: 'USERS_TABLE_NAME', 23 | partitionKey: 'domain', 24 | sortKey: 'email', 25 | indexes: { 26 | 'index-1': { 27 | partitionKey: 'domain', 28 | sortKey: 'email', 29 | }, 30 | }, 31 | }); 32 | 33 | const EntityManager = UserdataTableManager.entityManager(); 34 | 35 | async function test() { 36 | const entity = await EntityManager.put( 37 | new TableUser({ 38 | domain: 'test', 39 | email: 'not_empty', 40 | }), 41 | ); 42 | console.log(entity); 43 | } 44 | 45 | async function createTable() { 46 | const table = await UserdataTableManager.createTable(); 47 | console.log(table); 48 | } 49 | 50 | // createTable(); 51 | test(); 52 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | "target": "es2020", 6 | "lib": ["es2020"], 7 | "rootDir": ".", 8 | "baseUrl": ".", 9 | "experimentalDecorators": true, 10 | }, 11 | "include": ["."], 12 | "exclude": [] 13 | } -------------------------------------------------------------------------------- /lib/condition/types.ts: -------------------------------------------------------------------------------- 1 | export enum AttributeType { 2 | String = 'S', 3 | StringSet = 'SS', 4 | Number = 'N', 5 | NumberSet = 'NS', 6 | Binary = 'B', 7 | BinarySet = 'BS', 8 | Boolean = 'BOOL', 9 | Null = 'NULL', 10 | List = 'L', 11 | Map = 'M', 12 | } 13 | -------------------------------------------------------------------------------- /lib/decorators/helpers/customName.ts: -------------------------------------------------------------------------------- 1 | import Dynamode from '@lib/dynamode/index'; 2 | 3 | function customName(newNameEntity: string): ClassDecorator { 4 | return (entity: any) => { 5 | const oldNameEntity = entity.name; 6 | Object.defineProperty(entity, 'name', { 7 | writable: true, 8 | value: newNameEntity, 9 | }); 10 | Dynamode.storage.transferMetadata(oldNameEntity, newNameEntity); 11 | }; 12 | } 13 | 14 | export default customName; 15 | -------------------------------------------------------------------------------- /lib/decorators/helpers/dates.ts: -------------------------------------------------------------------------------- 1 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 2 | import { PrefixSuffixOptions } from '@lib/decorators/types'; 3 | 4 | export function stringDate( 5 | options?: PrefixSuffixOptions, 6 | ): >, K extends string>(Entity: T, propertyName: K) => void { 7 | return decorateAttribute(String, 'date', options); 8 | } 9 | 10 | export function numberDate(): >, K extends string>( 11 | Entity: T, 12 | propertyName: K, 13 | ) => void { 14 | return decorateAttribute(Number, 'date'); 15 | } 16 | -------------------------------------------------------------------------------- /lib/decorators/helpers/decorateAttribute.ts: -------------------------------------------------------------------------------- 1 | import { IndexDecoratorOptions, PrefixSuffixOptions } from '@lib/decorators/types'; 2 | import Dynamode from '@lib/dynamode/index'; 3 | import { AttributeIndexRole, AttributeRole, AttributeType } from '@lib/dynamode/storage/types'; 4 | 5 | export function decorateAttribute( 6 | type: AttributeType, 7 | role: Exclude | AttributeIndexRole, 8 | options?: PrefixSuffixOptions | IndexDecoratorOptions, 9 | ): (Entity: any, propertyName: string) => void { 10 | return (Entity: any, propertyName: string) => { 11 | const entityName = Entity.constructor.name; 12 | const prefix = options && 'prefix' in options ? options.prefix : undefined; 13 | const suffix = options && 'suffix' in options ? options.suffix : undefined; 14 | 15 | if (role === 'gsiPartitionKey' || role === 'gsiSortKey' || role === 'lsiSortKey') { 16 | const indexName = options && 'indexName' in options ? options.indexName : undefined; 17 | 18 | if (!indexName) { 19 | throw new Error(`Index name is required for ${role} attribute`); 20 | } 21 | 22 | return Dynamode.storage.registerIndex(entityName, propertyName, { 23 | propertyName, 24 | type, 25 | role: 'index', 26 | indexes: [{ name: indexName, role }], 27 | prefix, 28 | suffix, 29 | }); 30 | } 31 | 32 | Dynamode.storage.registerAttribute(entityName, propertyName, { 33 | propertyName, 34 | type, 35 | role, 36 | prefix, 37 | suffix, 38 | }); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/decorators/helpers/gsi.ts: -------------------------------------------------------------------------------- 1 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 2 | import { IndexDecoratorOptions, PrefixSuffixOptions } from '@lib/decorators/types'; 3 | 4 | export function stringGsiPartitionKey( 5 | options: IndexDecoratorOptions & PrefixSuffixOptions, 6 | ): >, K extends string>(Entity: T, propertyName: K) => void { 7 | return decorateAttribute(String, 'gsiPartitionKey', options); 8 | } 9 | 10 | export function numberGsiPartitionKey( 11 | options: IndexDecoratorOptions, 12 | ): >, K extends string>(Entity: T, propertyName: K) => void { 13 | return decorateAttribute(Number, 'gsiPartitionKey', options); 14 | } 15 | 16 | export function stringGsiSortKey( 17 | options: IndexDecoratorOptions & PrefixSuffixOptions, 18 | ): >, K extends string>(Entity: T, propertyName: K) => void { 19 | return decorateAttribute(String, 'gsiSortKey', options); 20 | } 21 | 22 | export function numberGsiSortKey( 23 | options: IndexDecoratorOptions, 24 | ): >, K extends string>(Entity: T, propertyName: K) => void { 25 | return decorateAttribute(Number, 'gsiSortKey', options); 26 | } 27 | -------------------------------------------------------------------------------- /lib/decorators/helpers/lsi.ts: -------------------------------------------------------------------------------- 1 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 2 | import { IndexDecoratorOptions, PrefixSuffixOptions } from '@lib/decorators/types'; 3 | 4 | export function stringLsiSortKey( 5 | options: IndexDecoratorOptions & PrefixSuffixOptions, 6 | ): >, K extends string>(Entity: T, propertyName: K) => void { 7 | return decorateAttribute(String, 'lsiSortKey', options); 8 | } 9 | 10 | export function numberLsiSortKey( 11 | options: IndexDecoratorOptions, 12 | ): >, K extends string>(Entity: T, propertyName: K) => void { 13 | return decorateAttribute(Number, 'lsiSortKey', options); 14 | } 15 | -------------------------------------------------------------------------------- /lib/decorators/helpers/other.ts: -------------------------------------------------------------------------------- 1 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 2 | import { PrefixSuffixOptions } from '@lib/decorators/types'; 3 | 4 | export function string( 5 | options?: PrefixSuffixOptions, 6 | ): >, K extends string>(Entity: T, propertyName: K) => void { 7 | return decorateAttribute(String, 'attribute', options); 8 | } 9 | 10 | export function number(): >, K extends string>(Entity: T, propertyName: K) => void { 11 | return decorateAttribute(Number, 'attribute'); 12 | } 13 | 14 | export function binary(): >, K extends string>( 15 | Entity: T, 16 | propertyName: K, 17 | ) => void { 18 | return decorateAttribute(Uint8Array, 'attribute'); 19 | } 20 | 21 | export function boolean(): >, K extends string>( 22 | Entity: T, 23 | propertyName: K, 24 | ) => void { 25 | return decorateAttribute(Boolean, 'attribute'); 26 | } 27 | 28 | export function object(): >>, K extends string>( 29 | Entity: T, 30 | propertyName: K, 31 | ) => void { 32 | return decorateAttribute(Object, 'attribute'); 33 | } 34 | 35 | export function array(): >>, K extends string>( 36 | Entity: T, 37 | propertyName: K, 38 | ) => void { 39 | return decorateAttribute(Array, 'attribute'); 40 | } 41 | 42 | export function set(): >>, K extends string>( 43 | Entity: T, 44 | propertyName: K, 45 | ) => void { 46 | return decorateAttribute(Set, 'attribute'); 47 | } 48 | 49 | export function map(): >>, K extends string>( 50 | Entity: T, 51 | propertyName: K, 52 | ) => void { 53 | return decorateAttribute(Map, 'attribute'); 54 | } 55 | -------------------------------------------------------------------------------- /lib/decorators/helpers/prefixSuffix.ts: -------------------------------------------------------------------------------- 1 | import Dynamode from '@lib/dynamode/index'; 2 | 3 | export function prefix(value: string) { 4 | return >, K extends string>(Entity: T, propertyName: K) => { 5 | const entityName = Entity.constructor.name; 6 | Dynamode.storage.updateAttributePrefix(entityName, propertyName, value); 7 | }; 8 | } 9 | 10 | export function suffix(value: string) { 11 | return >, K extends string>(Entity: T, propertyName: K) => { 12 | const entityName = Entity.constructor.name; 13 | Dynamode.storage.updateAttributeSuffix(entityName, propertyName, value); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /lib/decorators/helpers/primaryKey.ts: -------------------------------------------------------------------------------- 1 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 2 | import { PrefixSuffixOptions } from '@lib/decorators/types'; 3 | 4 | export function stringPartitionKey( 5 | options?: PrefixSuffixOptions, 6 | ): , K extends string>(Entity: T, propertyName: K) => void { 7 | return decorateAttribute(String, 'partitionKey', options); 8 | } 9 | 10 | export function numberPartitionKey(): , K extends string>( 11 | Entity: T, 12 | propertyName: K, 13 | ) => void { 14 | return decorateAttribute(Number, 'partitionKey'); 15 | } 16 | 17 | export function stringSortKey( 18 | options?: PrefixSuffixOptions, 19 | ): , K extends string>(Entity: T, propertyName: K) => void { 20 | return decorateAttribute(String, 'sortKey', options); 21 | } 22 | 23 | export function numberSortKey(): , K extends string>(Entity: T, propertyName: K) => void { 24 | return decorateAttribute(Number, 'sortKey'); 25 | } 26 | -------------------------------------------------------------------------------- /lib/decorators/index.ts: -------------------------------------------------------------------------------- 1 | import customName from '@lib/decorators/helpers/customName'; 2 | import { numberDate, stringDate } from '@lib/decorators/helpers/dates'; 3 | import { 4 | numberGsiPartitionKey, 5 | numberGsiSortKey, 6 | stringGsiPartitionKey, 7 | stringGsiSortKey, 8 | } from '@lib/decorators/helpers/gsi'; 9 | import { numberLsiSortKey, stringLsiSortKey } from '@lib/decorators/helpers/lsi'; 10 | import { array, binary, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other'; 11 | import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix'; 12 | import { 13 | numberPartitionKey, 14 | numberSortKey, 15 | stringPartitionKey, 16 | stringSortKey, 17 | } from '@lib/decorators/helpers/primaryKey'; 18 | 19 | const attribute = { 20 | string, 21 | number, 22 | boolean, 23 | object, 24 | array, 25 | set, 26 | map, 27 | binary, 28 | 29 | date: { 30 | string: stringDate, 31 | number: numberDate, 32 | }, 33 | partitionKey: { 34 | string: stringPartitionKey, 35 | number: numberPartitionKey, 36 | }, 37 | sortKey: { 38 | string: stringSortKey, 39 | number: numberSortKey, 40 | }, 41 | gsi: { 42 | partitionKey: { 43 | string: stringGsiPartitionKey, 44 | number: numberGsiPartitionKey, 45 | }, 46 | sortKey: { 47 | string: stringGsiSortKey, 48 | number: numberGsiSortKey, 49 | }, 50 | }, 51 | lsi: { 52 | sortKey: { 53 | string: stringLsiSortKey, 54 | number: numberLsiSortKey, 55 | }, 56 | }, 57 | 58 | prefix, 59 | suffix, 60 | }; 61 | 62 | const entity = { 63 | customName, 64 | }; 65 | 66 | export default attribute; 67 | export { entity }; 68 | -------------------------------------------------------------------------------- /lib/decorators/types.ts: -------------------------------------------------------------------------------- 1 | export type PrefixSuffixOptions = { 2 | prefix?: string; 3 | suffix?: string; 4 | }; 5 | 6 | export type IndexDecoratorOptions = { 7 | indexName: string; 8 | }; 9 | -------------------------------------------------------------------------------- /lib/dynamode/converter/index.ts: -------------------------------------------------------------------------------- 1 | import { convertToAttr, convertToNative, marshall, unmarshall } from '@aws-sdk/util-dynamodb'; 2 | 3 | type ConverterType = { 4 | marshall: typeof marshall; 5 | unmarshall: typeof unmarshall; 6 | convertToAttr: typeof convertToAttr; 7 | convertToNative: typeof convertToNative; 8 | }; 9 | 10 | const defaultConverter: ConverterType = { 11 | marshall, 12 | unmarshall, 13 | convertToAttr, 14 | convertToNative, 15 | }; 16 | 17 | let customConverter: ConverterType | undefined; 18 | const get = (): ConverterType => customConverter || defaultConverter; 19 | const set = (converter: ConverterType | undefined): void => { 20 | customConverter = converter; 21 | }; 22 | 23 | export default { 24 | get, 25 | set, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/dynamode/ddb/index.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from '@aws-sdk/client-dynamodb'; 2 | 3 | export type DDBType = { 4 | get: () => DynamoDB; 5 | set: (ddb: DynamoDB) => void; 6 | local: (endpoint?: string) => DynamoDB; 7 | }; 8 | 9 | export default function (): DDBType { 10 | let ddbInstance = new DynamoDB({}); 11 | 12 | const get = () => { 13 | return ddbInstance; 14 | }; 15 | 16 | const set = (ddb: DynamoDB): void => { 17 | ddbInstance = ddb; 18 | }; 19 | 20 | const local = (endpoint = 'http://localhost:8000'): DynamoDB => { 21 | set( 22 | new DynamoDB({ 23 | endpoint, 24 | region: 'local', 25 | credentials: { 26 | accessKeyId: 'local', 27 | secretAccessKey: 'local', 28 | }, 29 | }), 30 | ); 31 | return get(); 32 | }; 33 | 34 | return { 35 | get, 36 | set, 37 | local, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /lib/dynamode/index.ts: -------------------------------------------------------------------------------- 1 | import converter from '@lib/dynamode/converter'; 2 | import DDB, { DDBType } from '@lib/dynamode/ddb'; 3 | import separator, { SeparatorType } from '@lib/dynamode/separator'; 4 | import DynamodeStorage from '@lib/dynamode/storage'; 5 | 6 | class Dynamode { 7 | static default: Dynamode = new Dynamode(); 8 | 9 | public ddb: DDBType; 10 | public storage: DynamodeStorage; 11 | public converter: typeof converter; 12 | public separator: SeparatorType; 13 | 14 | constructor() { 15 | this.ddb = DDB(); 16 | this.storage = new DynamodeStorage(); 17 | this.converter = converter; 18 | this.separator = separator; 19 | } 20 | } 21 | 22 | export default Dynamode.default; 23 | -------------------------------------------------------------------------------- /lib/dynamode/separator/index.ts: -------------------------------------------------------------------------------- 1 | export type SeparatorType = { 2 | get: () => string; 3 | set: (separator: string) => void; 4 | }; 5 | 6 | let defaultConverter = '#'; 7 | 8 | const get = (): string => { 9 | return defaultConverter; 10 | }; 11 | 12 | const set = (separator: string): void => { 13 | defaultConverter = separator; 14 | }; 15 | 16 | export default { get, set }; 17 | -------------------------------------------------------------------------------- /lib/dynamode/storage/helpers/validator.ts: -------------------------------------------------------------------------------- 1 | import type { ValidateDecoratedAttribute, ValidateMetadataAttribute } from '@lib/dynamode/storage/types'; 2 | import { DYNAMODE_ALLOWED_KEY_TYPES, ValidationError } from '@lib/utils'; 3 | 4 | export function validateMetadataAttribute({ 5 | attributes, 6 | name, 7 | validRoles, 8 | indexName, 9 | entityName, 10 | }: ValidateMetadataAttribute): void { 11 | const attribute = attributes[name]; 12 | if (!attribute) { 13 | throw new ValidationError(`Attribute "${name}" should be decorated in "${entityName}" Entity.`); 14 | } 15 | 16 | if (!validRoles.includes(attribute.role)) { 17 | throw new ValidationError(`Attribute "${name}" is decorated with a wrong role in "${entityName}" Entity.`); 18 | } 19 | 20 | if (indexName && !attribute.indexes) { 21 | throw new ValidationError( 22 | `Attribute "${name}" should be decorated with index "${indexName}" in "${entityName}" Entity.`, 23 | ); 24 | } 25 | 26 | if (indexName && !attribute.indexes?.some((index) => index.name === indexName)) { 27 | throw new ValidationError( 28 | `Attribute "${name}" is not decorated with index "${indexName}" in "${entityName}" Entity.`, 29 | ); 30 | } 31 | 32 | if (!DYNAMODE_ALLOWED_KEY_TYPES.includes(attribute.type)) { 33 | throw new ValidationError(`Attribute "${name}" is decorated with invalid type in "${entityName}" Entity.`); 34 | } 35 | } 36 | 37 | export function validateDecoratedAttribute({ 38 | attribute, 39 | name, 40 | metadata, 41 | entityName, 42 | }: ValidateDecoratedAttribute): void { 43 | if (attribute.role === 'partitionKey' && metadata.partitionKey !== name) { 44 | throw new ValidationError( 45 | `Attribute "${name}" is not defined as a partition key in "${entityName}" Entity's metadata.`, 46 | ); 47 | } 48 | 49 | if (attribute.role === 'sortKey' && metadata.sortKey !== name) { 50 | throw new ValidationError(`Attribute "${name}" is not defined as a sort key in "${entityName}" Entity's metadata.`); 51 | } 52 | 53 | if (!attribute.indexes) { 54 | return; 55 | } 56 | 57 | attribute.indexes.forEach((index) => { 58 | if (index.role === 'gsiPartitionKey' && metadata.indexes?.[index.name]?.partitionKey !== name) { 59 | throw new ValidationError( 60 | `Attribute "${name}" is not defined as a GSI partition key in "${entityName}" Entity's metadata for index named "${index.name}".`, 61 | ); 62 | } 63 | 64 | if (index.role === 'gsiSortKey' && metadata.indexes?.[index.name]?.sortKey !== name) { 65 | throw new ValidationError( 66 | `Attribute "${name}" is not defined as a GSI sort key in "${entityName}" Entity's metadata for index named "${index.name}".`, 67 | ); 68 | } 69 | 70 | if (index.role === 'lsiSortKey' && metadata.indexes?.[index.name]?.sortKey !== name) { 71 | throw new ValidationError( 72 | `Attribute "${name}" is not defined as a LSI sort key in "${entityName}" Entity's metadata for index named "${index.name}".`, 73 | ); 74 | } 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /lib/dynamode/storage/types.ts: -------------------------------------------------------------------------------- 1 | import Entity from '@lib/entity'; 2 | import { Metadata } from '@lib/table/types'; 3 | 4 | export type IndexAttributeType = StringConstructor | NumberConstructor; 5 | export type TimestampAttributeType = StringConstructor | NumberConstructor; 6 | 7 | export type AttributeType = 8 | | StringConstructor 9 | | NumberConstructor 10 | | BooleanConstructor 11 | | ObjectConstructor 12 | | ArrayConstructor 13 | | SetConstructor 14 | | MapConstructor 15 | | Uint8ArrayConstructor; 16 | 17 | export type AttributeRole = 'partitionKey' | 'sortKey' | 'index' | 'date' | 'attribute' | 'dynamodeEntity'; 18 | export type AttributeIndexRole = 'gsiPartitionKey' | 'gsiSortKey' | 'lsiSortKey'; 19 | 20 | type BaseAttributeMetadata = { 21 | propertyName: string; 22 | type: AttributeType; 23 | prefix?: string; 24 | suffix?: string; 25 | }; 26 | 27 | export type IndexMetadata = { name: string; role: AttributeIndexRole }; 28 | 29 | export type AttributeMetadata = BaseAttributeMetadata & { 30 | role: AttributeRole; 31 | indexes?: IndexMetadata[]; 32 | }; 33 | 34 | export type AttributeIndexMetadata = BaseAttributeMetadata & { 35 | role: 'index'; 36 | indexes: IndexMetadata[]; 37 | }; 38 | 39 | export type AttributesMetadata = { 40 | [attributeName: string]: AttributeMetadata; 41 | }; 42 | 43 | export type EntityMetadata = { 44 | tableName: string; 45 | entity: typeof Entity; 46 | attributes: AttributesMetadata; 47 | }; 48 | 49 | export type EntitiesMetadata = { 50 | [entityName: string]: EntityMetadata; 51 | }; 52 | 53 | export type TableMetadata = { 54 | tableEntity: typeof Entity; 55 | metadata: Metadata; 56 | }; 57 | 58 | export type TablesMetadata = { 59 | [tableName: string]: TableMetadata; 60 | }; 61 | 62 | // helpers 63 | 64 | export type ValidateMetadataAttribute = { 65 | entityName: string; 66 | name: string; 67 | attributes: AttributesMetadata; 68 | validRoles: AttributeRole[]; 69 | indexName?: string; 70 | }; 71 | 72 | export type ValidateDecoratedAttribute = { 73 | entityName: string; 74 | name: string; 75 | attribute: AttributeMetadata; 76 | metadata: Metadata; 77 | }; 78 | -------------------------------------------------------------------------------- /lib/entity/helpers/buildExpressions.ts: -------------------------------------------------------------------------------- 1 | import Condition from '@lib/condition'; 2 | import Entity from '@lib/entity'; 3 | import { buildProjectionOperators, buildUpdateOperators } from '@lib/entity/helpers/buildOperators'; 4 | import { 5 | BuildDeleteConditionExpression, 6 | BuildGetProjectionExpression, 7 | BuildPutConditionExpression, 8 | BuildUpdateConditionExpression, 9 | EntityKey, 10 | UpdateProps, 11 | } from '@lib/entity/types'; 12 | import { AttributeNames, ExpressionBuilder, isNotEmpty, isNotEmptyString } from '@lib/utils'; 13 | 14 | export function buildGetProjectionExpression( 15 | attributes?: Array>, 16 | attributeNames: AttributeNames = {}, 17 | ): BuildGetProjectionExpression { 18 | if (!attributes || attributes.length === 0) { 19 | return {}; 20 | } 21 | 22 | const operators = buildProjectionOperators(attributes); 23 | const projectionExpression = new ExpressionBuilder({ attributeNames }).run(operators); 24 | 25 | return { 26 | projectionExpression: isNotEmptyString(projectionExpression) ? projectionExpression : undefined, 27 | attributeNames: isNotEmpty(attributeNames) ? attributeNames : undefined, 28 | }; 29 | } 30 | 31 | export function buildUpdateConditionExpression( 32 | entity: E, 33 | props: UpdateProps, 34 | optionsCondition?: Condition, 35 | ): BuildUpdateConditionExpression { 36 | const expressionsBuilder = new ExpressionBuilder(); 37 | const operators = buildUpdateOperators(entity, props); 38 | 39 | return { 40 | updateExpression: expressionsBuilder.run(operators), 41 | conditionExpression: optionsCondition ? expressionsBuilder.run(optionsCondition['operators']) : undefined, 42 | attributeNames: expressionsBuilder.attributeNames, 43 | attributeValues: expressionsBuilder.attributeValues, 44 | }; 45 | } 46 | 47 | export function buildPutConditionExpression( 48 | overwriteCondition?: Condition, 49 | optionsCondition?: Condition, 50 | ): BuildPutConditionExpression { 51 | const expressionsBuilder = new ExpressionBuilder(); 52 | const conditions = overwriteCondition?.condition(optionsCondition) || optionsCondition?.condition(overwriteCondition); 53 | const conditionExpression = expressionsBuilder.run(conditions?.['operators'] || []); 54 | 55 | return { 56 | conditionExpression: isNotEmptyString(conditionExpression) ? conditionExpression : undefined, 57 | attributeNames: expressionsBuilder.attributeNames, 58 | attributeValues: expressionsBuilder.attributeValues, 59 | }; 60 | } 61 | 62 | export function buildDeleteConditionExpression( 63 | notExistsCondition?: Condition, 64 | optionsCondition?: Condition, 65 | ): BuildDeleteConditionExpression { 66 | const expressionsBuilder = new ExpressionBuilder(); 67 | const conditions = notExistsCondition?.condition(optionsCondition) || optionsCondition?.condition(notExistsCondition); 68 | const conditionExpression = expressionsBuilder.run(conditions?.['operators'] || []); 69 | 70 | return { 71 | conditionExpression: isNotEmptyString(conditionExpression) ? conditionExpression : undefined, 72 | attributeNames: expressionsBuilder.attributeNames, 73 | attributeValues: expressionsBuilder.attributeValues, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /lib/entity/helpers/returnValues.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReturnValue as DynamoReturnValue, 3 | ReturnValuesOnConditionCheckFailure as DynamoReturnValueOnFailure, 4 | } from '@aws-sdk/client-dynamodb'; 5 | import { ReturnValues, ReturnValuesLimited } from '@lib/entity/types'; 6 | 7 | export function mapReturnValues(returnValues?: ReturnValues): DynamoReturnValue { 8 | if (!returnValues) { 9 | return DynamoReturnValue.ALL_NEW; 10 | } 11 | 12 | return ( 13 | { 14 | none: DynamoReturnValue.NONE, 15 | allOld: DynamoReturnValue.ALL_OLD, 16 | allNew: DynamoReturnValue.ALL_NEW, 17 | updatedOld: DynamoReturnValue.UPDATED_OLD, 18 | updatedNew: DynamoReturnValue.UPDATED_NEW, 19 | } as const 20 | )[returnValues]; 21 | } 22 | 23 | export function mapReturnValuesLimited(returnValues?: ReturnValuesLimited): DynamoReturnValueOnFailure { 24 | if (!returnValues) { 25 | return DynamoReturnValueOnFailure.ALL_OLD; 26 | } 27 | 28 | return ( 29 | { 30 | none: DynamoReturnValueOnFailure.NONE, 31 | allOld: DynamoReturnValueOnFailure.ALL_OLD, 32 | } as const 33 | )[returnValues]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/entity/helpers/transformValues.ts: -------------------------------------------------------------------------------- 1 | import Dynamode from '@lib/dynamode/index'; 2 | import Entity from '@lib/entity'; 3 | import { InvalidParameter } from '@lib/utils'; 4 | 5 | export function prefixSuffixValue(entity: E, key: string, value: unknown): unknown { 6 | if (typeof value !== 'string') { 7 | return value; 8 | } 9 | 10 | const attributes = Dynamode.storage.getEntityAttributes(entity.name); 11 | const separator = Dynamode.separator.get(); 12 | const prefix = attributes[key]?.prefix || ''; 13 | const suffix = attributes[key]?.suffix || ''; 14 | 15 | return [prefix, value, suffix].filter((p) => p).join(separator); 16 | } 17 | 18 | export function truncateValue(entity: E, key: string, value: unknown): unknown { 19 | if (typeof value !== 'string') { 20 | return value; 21 | } 22 | 23 | const attributes = Dynamode.storage.getEntityAttributes(entity.name); 24 | const separator = Dynamode.separator.get(); 25 | const prefix = attributes[key].prefix || ''; 26 | const suffix = attributes[key].suffix || ''; 27 | 28 | const valueSections = value.split(separator); 29 | 30 | if (valueSections.at(0) === prefix) { 31 | valueSections.shift(); 32 | } 33 | 34 | if (valueSections.at(-1) === suffix) { 35 | valueSections.pop(); 36 | } 37 | 38 | return valueSections.join(separator); 39 | } 40 | 41 | export function transformDateValue(entity: E, key: string, value: unknown): unknown { 42 | const attributes = Dynamode.storage.getEntityAttributes(entity.name); 43 | const attribute = attributes[key]; 44 | 45 | if (value instanceof Date) { 46 | if (attribute.role !== 'date') { 47 | throw new InvalidParameter('Invalid date attribute role'); 48 | } 49 | 50 | switch (attribute.type) { 51 | case String: { 52 | return value.toISOString(); 53 | } 54 | case Number: { 55 | return value.getTime(); 56 | } 57 | default: { 58 | throw new InvalidParameter('Invalid date attribute type'); 59 | } 60 | } 61 | } 62 | 63 | return value; 64 | } 65 | 66 | export function transformValue(entity: E, key: string, value: unknown): unknown { 67 | const processedValue = transformDateValue(entity, key, value); 68 | return prefixSuffixValue(entity, key, processedValue); 69 | } 70 | -------------------------------------------------------------------------------- /lib/entity/index.ts: -------------------------------------------------------------------------------- 1 | import Dynamode from '@lib/dynamode/index'; 2 | import { DYNAMODE_ENTITY } from '@lib/utils'; 3 | 4 | export default class Entity { 5 | public readonly dynamodeEntity!: string; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars 8 | constructor(...args: unknown[]) { 9 | this.dynamodeEntity = this.constructor.name; 10 | } 11 | } 12 | 13 | Dynamode.storage.registerAttribute(Entity.name, DYNAMODE_ENTITY, { 14 | propertyName: DYNAMODE_ENTITY, 15 | type: String, 16 | role: DYNAMODE_ENTITY, 17 | }); 18 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as dynamode from '@lib/module'; 2 | 3 | export default dynamode; 4 | export * from '@lib/module'; 5 | -------------------------------------------------------------------------------- /lib/module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-definitions */ 2 | import Condition, { AttributeType } from '@lib/condition'; 3 | import attribute, { entity } from '@lib/decorators'; 4 | import Dynamode from '@lib/dynamode/index'; 5 | import Entity from '@lib/entity'; 6 | import Query from '@lib/query'; 7 | import Scan from '@lib/scan'; 8 | import Stream from '@lib/stream'; 9 | import TableManager from '@lib/table'; 10 | import transactionGet from '@lib/transactionGet'; 11 | import transactionWrite from '@lib/transactionWrite'; 12 | 13 | ///// --- https://github.com/aws/aws-sdk-js-v3/issues/2125 --- 14 | // some @aws-sdk clients references these DOM lib interfaces, 15 | // so we need them to exist to compile without having DOM. 16 | declare global { 17 | /* eslint-disable @typescript-eslint/no-empty-interface */ 18 | interface ReadableStream {} 19 | interface File {} 20 | } 21 | 22 | export { 23 | //Condition 24 | Condition, 25 | AttributeType, 26 | 27 | //decorators 28 | attribute, 29 | entity, 30 | 31 | //Entity 32 | Entity, 33 | 34 | //table manager 35 | TableManager, 36 | 37 | //Query 38 | Query, 39 | 40 | //Scan 41 | Scan, 42 | 43 | //Dynamode 44 | Dynamode, 45 | 46 | //transactions 47 | transactionGet, 48 | transactionWrite, 49 | 50 | //Stream 51 | Stream, 52 | }; 53 | -------------------------------------------------------------------------------- /lib/query/types.ts: -------------------------------------------------------------------------------- 1 | import { QueryInput } from '@aws-sdk/client-dynamodb'; 2 | import Entity from '@lib/entity'; 3 | import type { ReturnOption } from '@lib/entity/types'; 4 | import { Metadata, TableRetrieverLastKey } from '@lib/table/types'; 5 | import { AttributeNames, AttributeValues } from '@lib/utils'; 6 | 7 | export type QueryRunOptions = { 8 | extraInput?: Partial; 9 | return?: ReturnOption; 10 | all?: boolean; 11 | delay?: number; 12 | max?: number; 13 | }; 14 | 15 | export type QueryRunOutput, E extends typeof Entity> = { 16 | items: Array>; 17 | count: number; 18 | scannedCount: number; 19 | lastKey?: TableRetrieverLastKey; 20 | }; 21 | 22 | export type BuildQueryConditionExpression = { 23 | attributeNames: AttributeNames; 24 | attributeValues: AttributeValues; 25 | conditionExpression: string; 26 | keyConditionExpression: string; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/retriever/index.ts: -------------------------------------------------------------------------------- 1 | import { QueryInput, ScanInput } from '@aws-sdk/client-dynamodb'; 2 | import Condition from '@lib/condition'; 3 | import Dynamode from '@lib/dynamode/index'; 4 | import Entity from '@lib/entity'; 5 | import { buildGetProjectionExpression } from '@lib/entity/helpers/buildExpressions'; 6 | import { convertRetrieverLastKeyToAttributeValues } from '@lib/entity/helpers/converters'; 7 | import { EntityKey } from '@lib/entity/types'; 8 | import { Metadata, TableIndexNames, TableRetrieverLastKey } from '@lib/table/types'; 9 | import { AttributeNames, AttributeValues } from '@lib/utils'; 10 | 11 | export default class RetrieverBase, E extends typeof Entity> extends Condition { 12 | protected input: QueryInput | ScanInput; 13 | protected attributeNames: AttributeNames = {}; 14 | protected attributeValues: AttributeValues = {}; 15 | 16 | constructor(entity: E) { 17 | super(entity); 18 | this.input = { 19 | TableName: Dynamode.storage.getEntityTableName(entity.name), 20 | }; 21 | } 22 | 23 | public indexName(name: TableIndexNames) { 24 | this.input.IndexName = String(name); 25 | return this; 26 | } 27 | 28 | public limit(count: number) { 29 | this.input.Limit = count; 30 | return this; 31 | } 32 | 33 | public startAt(key?: TableRetrieverLastKey) { 34 | if (key) { 35 | this.input.ExclusiveStartKey = convertRetrieverLastKeyToAttributeValues(this.entity, key); 36 | } 37 | 38 | return this; 39 | } 40 | 41 | public consistent() { 42 | this.input.ConsistentRead = true; 43 | return this; 44 | } 45 | 46 | public count() { 47 | this.input.Select = 'COUNT'; 48 | return this; 49 | } 50 | 51 | public attributes(attributes: Array>) { 52 | this.input.ProjectionExpression = buildGetProjectionExpression( 53 | attributes, 54 | this.attributeNames, 55 | ).projectionExpression; 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/scan/index.ts: -------------------------------------------------------------------------------- 1 | import { ScanCommandOutput, ScanInput } from '@aws-sdk/client-dynamodb'; 2 | import Dynamode from '@lib/dynamode/index'; 3 | import Entity from '@lib/entity'; 4 | import { convertAttributeValuesToEntity, convertAttributeValuesToLastKey } from '@lib/entity/helpers/converters'; 5 | import RetrieverBase from '@lib/retriever'; 6 | import type { ScanRunOptions, ScanRunOutput } from '@lib/scan/types'; 7 | import { Metadata } from '@lib/table/types'; 8 | import { ExpressionBuilder, isNotEmptyString } from '@lib/utils'; 9 | 10 | export default class Scan, E extends typeof Entity> extends RetrieverBase { 11 | protected declare input: ScanInput; 12 | 13 | constructor(entity: E) { 14 | super(entity); 15 | } 16 | 17 | public run(options?: ScanRunOptions & { return?: 'default' }): Promise>; 18 | public run(options: ScanRunOptions & { return: 'output' }): Promise; 19 | public run(options: ScanRunOptions & { return: 'input' }): ScanInput; 20 | public run(options?: ScanRunOptions): Promise | ScanCommandOutput> | ScanInput { 21 | this.buildScanInput(options?.extraInput); 22 | 23 | if (options?.return === 'input') { 24 | return this.input; 25 | } 26 | 27 | return (async () => { 28 | const result = await Dynamode.ddb.get().scan(this.input); 29 | 30 | if (options?.return === 'output') { 31 | return result; 32 | } 33 | 34 | const items = result.Items || []; 35 | 36 | return { 37 | items: items.map((item) => convertAttributeValuesToEntity(this.entity, item)), 38 | count: result.Count || 0, 39 | scannedCount: result.ScannedCount || 0, 40 | lastKey: result.LastEvaluatedKey 41 | ? convertAttributeValuesToLastKey(this.entity, result.LastEvaluatedKey) 42 | : undefined, 43 | }; 44 | })(); 45 | } 46 | 47 | public segment(value: number) { 48 | this.input.Segment = value; 49 | return this; 50 | } 51 | 52 | public totalSegments(value: number) { 53 | this.input.TotalSegments = value; 54 | return this; 55 | } 56 | 57 | private buildScanInput(extraInput?: Partial) { 58 | const expressionBuilder = new ExpressionBuilder({ 59 | attributeNames: this.attributeNames, 60 | attributeValues: this.attributeValues, 61 | }); 62 | const conditionExpression = expressionBuilder.run(this.operators); 63 | 64 | this.input = { 65 | ...this.input, 66 | FilterExpression: isNotEmptyString(conditionExpression) ? conditionExpression : undefined, 67 | ExpressionAttributeNames: expressionBuilder.attributeNames, 68 | ExpressionAttributeValues: expressionBuilder.attributeValues, 69 | ...extraInput, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/scan/types.ts: -------------------------------------------------------------------------------- 1 | import { ScanInput } from '@aws-sdk/client-dynamodb'; 2 | import Entity from '@lib/entity'; 3 | import type { ReturnOption } from '@lib/entity/types'; 4 | import { Metadata, TableRetrieverLastKey } from '@lib/table/types'; 5 | import { AttributeNames, AttributeValues } from '@lib/utils'; 6 | 7 | export type ScanRunOptions = { 8 | extraInput?: Partial; 9 | return?: ReturnOption; 10 | }; 11 | 12 | export type ScanRunOutput, E extends typeof Entity> = { 13 | items: Array>; 14 | count: number; 15 | scannedCount: number; 16 | lastKey?: TableRetrieverLastKey; 17 | }; 18 | 19 | export type BuildScanConditionExpression = { 20 | attributeNames: AttributeNames; 21 | attributeValues: AttributeValues; 22 | conditionExpression: string; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/stream/index.ts: -------------------------------------------------------------------------------- 1 | import Dynamode from '@lib/dynamode/index'; 2 | import Entity from '@lib/entity'; 3 | import { convertAttributeValuesToEntity } from '@lib/entity/helpers/converters'; 4 | import { AttributeValues, DynamodeStreamError, fromDynamo } from '@lib/utils'; 5 | 6 | import { DynamoDBRecord } from './types'; 7 | 8 | export default class Stream { 9 | streamType: 'newImage' | 'oldImage' | 'both'; 10 | operation: 'insert' | 'modify' | 'remove'; 11 | oldImage?: InstanceType; 12 | newImage?: InstanceType; 13 | 14 | // Dynamode entity class 15 | entity: E; 16 | 17 | constructor({ dynamodb: record, eventName }: DynamoDBRecord) { 18 | switch (eventName) { 19 | case 'INSERT': 20 | this.operation = 'insert'; 21 | break; 22 | case 'MODIFY': 23 | this.operation = 'modify'; 24 | break; 25 | case 'REMOVE': 26 | this.operation = 'remove'; 27 | break; 28 | default: 29 | throw new DynamodeStreamError('Invalid operation'); 30 | } 31 | 32 | if (!record) { 33 | throw new DynamodeStreamError('Invalid record'); 34 | } 35 | 36 | switch (record.StreamViewType) { 37 | case 'KEYS_ONLY': 38 | throw new DynamodeStreamError("Stream of 'KEYS_ONLY' type is not supported"); 39 | case 'NEW_IMAGE': 40 | this.streamType = 'newImage'; 41 | break; 42 | case 'OLD_IMAGE': 43 | this.streamType = 'oldImage'; 44 | break; 45 | case 'NEW_AND_OLD_IMAGES': 46 | this.streamType = 'both'; 47 | break; 48 | default: 49 | throw new DynamodeStreamError('Invalid streamType'); 50 | } 51 | 52 | const item = fromDynamo((record.NewImage as AttributeValues) ?? (record.OldImage as AttributeValues) ?? {}); 53 | const dynamodeEntity = item?.dynamodeEntity; 54 | 55 | if (!dynamodeEntity || typeof dynamodeEntity !== 'string') { 56 | throw new DynamodeStreamError("Processed item isn't a Dynamode entity"); 57 | } 58 | 59 | this.entity = Dynamode.storage.getEntityClass(dynamodeEntity) as E; 60 | 61 | if (record.OldImage) { 62 | this.oldImage = convertAttributeValuesToEntity(this.entity, record.OldImage as AttributeValues); 63 | } 64 | if (record.NewImage) { 65 | this.newImage = convertAttributeValuesToEntity(this.entity, record.NewImage as AttributeValues); 66 | } 67 | } 68 | 69 | isEntity(entity: TargetEntity): this is Stream { 70 | return this.entity === entity; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/stream/types.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeValue } from '@aws-sdk/client-dynamodb'; 2 | 3 | // For compatibility with aws lambda: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/dynamodb-stream.d.ts 4 | type AttributeValueAWSLambda = { 5 | B?: string | undefined; 6 | BS?: string[] | undefined; 7 | BOOL?: boolean | undefined; 8 | L?: AttributeValueAWSLambda[] | undefined; 9 | M?: { [id: string]: AttributeValueAWSLambda } | undefined; 10 | N?: string | undefined; 11 | NS?: string[] | undefined; 12 | NULL?: boolean | undefined; 13 | S?: string | undefined; 14 | SS?: string[] | undefined; 15 | }; 16 | 17 | export type StreamPayload = { 18 | NewImage?: Record | undefined; 19 | OldImage?: Record | undefined; 20 | StreamViewType?: 'KEYS_ONLY' | 'NEW_IMAGE' | 'OLD_IMAGE' | 'NEW_AND_OLD_IMAGES' | undefined; 21 | }; 22 | 23 | export type DynamoDBRecord = { 24 | eventName?: 'INSERT' | 'MODIFY' | 'REMOVE' | undefined; 25 | dynamodb?: StreamPayload | undefined; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/table/helpers/builders.ts: -------------------------------------------------------------------------------- 1 | import { GlobalSecondaryIndexUpdate } from '@aws-sdk/client-dynamodb'; 2 | import { getKeySchema } from '@lib/table/helpers/schema'; 3 | import { BuildIndexCreate } from '@lib/table/types'; 4 | 5 | export function buildIndexCreate({ 6 | indexName, 7 | partitionKey, 8 | sortKey, 9 | throughput, 10 | }: BuildIndexCreate): GlobalSecondaryIndexUpdate[] { 11 | return [ 12 | { 13 | Create: { 14 | IndexName: indexName, 15 | KeySchema: getKeySchema(partitionKey, sortKey), 16 | Projection: { ProjectionType: 'ALL' }, 17 | ProvisionedThroughput: throughput, 18 | }, 19 | }, 20 | ]; 21 | } 22 | 23 | export function buildIndexDelete(indexName: string): GlobalSecondaryIndexUpdate[] { 24 | return [ 25 | { 26 | Delete: { 27 | IndexName: indexName, 28 | }, 29 | }, 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /lib/table/helpers/converters.ts: -------------------------------------------------------------------------------- 1 | import { TableDescription } from '@aws-sdk/client-dynamodb'; 2 | import { TableData } from '@lib/table/types'; 3 | import { ValidationError } from '@lib/utils'; 4 | 5 | export function convertToTableData({ 6 | TableName, 7 | TableStatus, 8 | AttributeDefinitions, 9 | KeySchema, 10 | ItemCount = 0, 11 | TableSizeBytes = 0, 12 | BillingModeSummary, 13 | CreationDateTime, 14 | LocalSecondaryIndexes = [], 15 | GlobalSecondaryIndexes = [], 16 | }: TableDescription = {}): TableData { 17 | if (!TableName || !TableStatus || !CreationDateTime || !AttributeDefinitions || !KeySchema) { 18 | throw new ValidationError('Table description is invalid.'); 19 | } 20 | 21 | return { 22 | tableName: TableName, 23 | status: TableStatus, 24 | attributeDefinitions: AttributeDefinitions.map((ad) => { 25 | if (!ad.AttributeName || !ad.AttributeType) { 26 | throw new ValidationError('Attribute Definition is invalid.'); 27 | } 28 | return { name: ad.AttributeName, type: ad.AttributeType }; 29 | }), 30 | keySchema: KeySchema.map((ks) => { 31 | if (!ks.AttributeName || !ks.KeyType) { 32 | throw new ValidationError('Key schema is invalid.'); 33 | } 34 | return { name: ks.AttributeName, type: ks.KeyType }; 35 | }), 36 | itemCount: ItemCount, 37 | tableSizeBytes: TableSizeBytes, 38 | billingMode: BillingModeSummary?.BillingMode, 39 | creationTime: CreationDateTime, 40 | localSecondaryIndexes: LocalSecondaryIndexes?.map((lsi) => { 41 | if (!lsi.IndexName || !lsi.KeySchema) { 42 | throw new ValidationError('Local Secondary Index is invalid.'); 43 | } 44 | return { 45 | indexName: lsi.IndexName, 46 | keySchema: lsi.KeySchema.map((ks) => { 47 | if (!ks.AttributeName || !ks.KeyType) { 48 | throw new ValidationError('Key schema is invalid.'); 49 | } 50 | return { name: ks.AttributeName, type: ks.KeyType }; 51 | }), 52 | itemCount: lsi.ItemCount ?? 0, 53 | }; 54 | }), 55 | globalSecondaryIndexes: GlobalSecondaryIndexes?.map((lsi) => { 56 | if (!lsi.IndexName || !lsi.KeySchema) { 57 | throw new ValidationError('Local Secondary Index is invalid.'); 58 | } 59 | return { 60 | indexName: lsi.IndexName, 61 | keySchema: lsi.KeySchema.map((ks) => { 62 | if (!ks.AttributeName || !ks.KeyType) { 63 | throw new ValidationError('Key schema is invalid.'); 64 | } 65 | return { name: ks.AttributeName, type: ks.KeyType }; 66 | }), 67 | itemCount: lsi.ItemCount ?? 0, 68 | }; 69 | }), 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /lib/table/helpers/definitions.ts: -------------------------------------------------------------------------------- 1 | import { AttributeDefinition } from '@aws-sdk/client-dynamodb'; 2 | import Dynamode from '@lib/dynamode'; 3 | import Entity from '@lib/entity'; 4 | import { getAttributeType } from '@lib/table/helpers/utils'; 5 | import { Metadata } from '@lib/table/types'; 6 | 7 | export function getTableAttributeDefinitions, TE extends typeof Entity>( 8 | metadata: M, 9 | tableEntityName: string, 10 | ): AttributeDefinition[] { 11 | const definitions: AttributeDefinition[] = []; 12 | const attributes = Dynamode.storage.getEntityAttributes(tableEntityName); 13 | const { partitionKey, sortKey } = metadata; 14 | 15 | definitions.push({ 16 | AttributeName: partitionKey, 17 | AttributeType: getAttributeType(attributes, partitionKey), 18 | }); 19 | 20 | if (sortKey) { 21 | definitions.push({ 22 | AttributeName: sortKey, 23 | AttributeType: getAttributeType(attributes, sortKey), 24 | }); 25 | } 26 | 27 | if (metadata.indexes) { 28 | Object.values(metadata.indexes).forEach((index) => { 29 | const { partitionKey, sortKey } = index; 30 | 31 | if (partitionKey) { 32 | definitions.push({ 33 | AttributeName: partitionKey, 34 | AttributeType: getAttributeType(attributes, partitionKey), 35 | }); 36 | } 37 | 38 | if (sortKey) { 39 | definitions.push({ 40 | AttributeName: sortKey, 41 | AttributeType: getAttributeType(attributes, sortKey), 42 | }); 43 | } 44 | }); 45 | } 46 | 47 | const uniqueDefinitions = [ 48 | ...new Map(definitions.map((definition) => [definition.AttributeName, definition])).values(), 49 | ]; 50 | 51 | return uniqueDefinitions; 52 | } 53 | -------------------------------------------------------------------------------- /lib/table/helpers/indexes.ts: -------------------------------------------------------------------------------- 1 | import { LocalSecondaryIndex } from '@aws-sdk/client-dynamodb'; 2 | import Entity from '@lib/entity'; 3 | import { getKeySchema } from '@lib/table/helpers/schema'; 4 | import { Metadata } from '@lib/table/types'; 5 | 6 | export function getTableLocalSecondaryIndexes, TE extends typeof Entity>( 7 | metadata: M, 8 | ): LocalSecondaryIndex[] { 9 | const { partitionKey, indexes = {} } = metadata; 10 | 11 | return Object.entries(indexes) 12 | .filter(([, index]) => !index.partitionKey && index.sortKey) 13 | .map(([indexName, index]) => ({ 14 | IndexName: indexName, 15 | KeySchema: getKeySchema(partitionKey, index.sortKey), 16 | Projection: { ProjectionType: 'ALL' }, 17 | })); 18 | } 19 | 20 | export function getTableGlobalSecondaryIndexes, TE extends typeof Entity>( 21 | metadata: M, 22 | ): LocalSecondaryIndex[] { 23 | const { indexes = {} } = metadata; 24 | 25 | return Object.entries(indexes) 26 | .filter(([, index]) => !!index.partitionKey) 27 | .map(([indexName, index]) => ({ 28 | IndexName: indexName, 29 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 30 | KeySchema: getKeySchema(index.partitionKey!, index.sortKey), 31 | Projection: { ProjectionType: 'ALL' }, 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /lib/table/helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import { KeySchemaElement } from '@aws-sdk/client-dynamodb'; 2 | 3 | export function getKeySchema(partitionKey: string, sortKey?: string): KeySchemaElement[] { 4 | const schema: KeySchemaElement[] = [{ AttributeName: partitionKey, KeyType: 'HASH' }]; 5 | 6 | if (sortKey) { 7 | schema.push({ AttributeName: sortKey, KeyType: 'RANGE' }); 8 | } 9 | 10 | return schema; 11 | } 12 | -------------------------------------------------------------------------------- /lib/table/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { AttributesMetadata } from '@lib/dynamode/storage/types'; 2 | import { ConflictError, deepEqual, DYNAMODE_DYNAMO_KEY_TYPE_MAP, ValidationError } from '@lib/utils'; 3 | 4 | export function compareDynamodeEntityWithDynamoTable(dynamodeSchema: T[], ddbSchema: T[]): void { 5 | dynamodeSchema.forEach((a) => { 6 | if (!ddbSchema?.some((b) => deepEqual(a, b))) { 7 | throw new ConflictError(`Key "${JSON.stringify(a)}" not found in table`); 8 | } 9 | }); 10 | 11 | ddbSchema.forEach((a) => { 12 | if (!dynamodeSchema.some((b) => deepEqual(a, b))) { 13 | throw new ConflictError(`Key "${JSON.stringify(a)}" not found in entity`); 14 | } 15 | }); 16 | } 17 | 18 | export function getAttributeType(attributes: AttributesMetadata, attribute: string): 'S' | 'N' { 19 | const attributeType = DYNAMODE_DYNAMO_KEY_TYPE_MAP.get(attributes[attribute].type); 20 | 21 | if (!attributeType) { 22 | throw new ValidationError(`Attribute "${attribute}" is decorated with invalid type.`); 23 | } 24 | 25 | return attributeType; 26 | } 27 | -------------------------------------------------------------------------------- /lib/table/helpers/validator.ts: -------------------------------------------------------------------------------- 1 | import Entity from '@lib/entity'; 2 | import { getTableAttributeDefinitions } from '@lib/table/helpers/definitions'; 3 | import { getTableGlobalSecondaryIndexes, getTableLocalSecondaryIndexes } from '@lib/table/helpers/indexes'; 4 | import { getKeySchema } from '@lib/table/helpers/schema'; 5 | import { compareDynamodeEntityWithDynamoTable } from '@lib/table/helpers/utils'; 6 | import { Metadata, ValidateTableSync } from '@lib/table/types'; 7 | 8 | export function validateTable, TE extends typeof Entity>({ 9 | metadata, 10 | tableNameEntity, 11 | table = {}, 12 | }: ValidateTableSync): void { 13 | const { 14 | LocalSecondaryIndexes = [], 15 | GlobalSecondaryIndexes = [], 16 | AttributeDefinitions: tableAttributeDefinitions = [], 17 | KeySchema: tableKeySchema = [], 18 | } = table; 19 | 20 | const tableLocalSecondaryIndexes = LocalSecondaryIndexes.map((v) => ({ 21 | IndexName: v.IndexName, 22 | KeySchema: v.KeySchema, 23 | })); 24 | const tableGlobalSecondaryIndexes = GlobalSecondaryIndexes.map((v) => ({ 25 | IndexName: v.IndexName, 26 | KeySchema: v.KeySchema, 27 | })); 28 | const keySchema = getKeySchema(metadata.partitionKey, metadata.sortKey); 29 | const attributeDefinitions = getTableAttributeDefinitions(metadata, tableNameEntity); 30 | const localSecondaryIndexes = getTableLocalSecondaryIndexes(metadata).map((v) => ({ 31 | IndexName: v.IndexName, 32 | KeySchema: v.KeySchema, 33 | })); 34 | const globalSecondaryIndexes = getTableGlobalSecondaryIndexes(metadata).map((v) => ({ 35 | IndexName: v.IndexName, 36 | KeySchema: v.KeySchema, 37 | })); 38 | 39 | compareDynamodeEntityWithDynamoTable(keySchema, tableKeySchema); 40 | compareDynamodeEntityWithDynamoTable(attributeDefinitions, tableAttributeDefinitions); 41 | compareDynamodeEntityWithDynamoTable(localSecondaryIndexes, tableLocalSecondaryIndexes); 42 | compareDynamodeEntityWithDynamoTable(globalSecondaryIndexes, tableGlobalSecondaryIndexes); 43 | } 44 | -------------------------------------------------------------------------------- /lib/transactionGet/index.ts: -------------------------------------------------------------------------------- 1 | import { TransactGetItemsCommandInput, TransactGetItemsOutput } from '@aws-sdk/client-dynamodb'; 2 | import Dynamode from '@lib/dynamode/index'; 3 | import Entity from '@lib/entity'; 4 | import { convertAttributeValuesToEntity } from '@lib/entity/helpers/converters'; 5 | import type { TransactionGetInput, TransactionGetOptions, TransactionGetOutput } from '@lib/transactionGet/types'; 6 | import { NotFoundError } from '@lib/utils'; 7 | 8 | export default function transactionGet>( 9 | transactions: TransactionGetInput<[...E]>, 10 | ): Promise>; 11 | 12 | export default function transactionGet>( 13 | transactions: TransactionGetInput<[...E]>, 14 | options: TransactionGetOptions & { 15 | return?: 'default'; 16 | throwOnNotFound?: true; 17 | }, 18 | ): Promise>; 19 | 20 | export default function transactionGet>( 21 | transactions: TransactionGetInput<[...E]>, 22 | options: TransactionGetOptions & { 23 | return?: 'default'; 24 | throwOnNotFound: false; 25 | }, 26 | ): Promise>; 27 | 28 | export default function transactionGet>( 29 | transactions: TransactionGetInput<[...E]>, 30 | options: TransactionGetOptions & { return: 'output' }, 31 | ): Promise; 32 | 33 | export default function transactionGet>( 34 | transactions: TransactionGetInput<[...E]>, 35 | options: TransactionGetOptions & { return: 'input' }, 36 | ): TransactGetItemsCommandInput; 37 | 38 | export default function transactionGet>( 39 | transactions: TransactionGetInput<[...E]>, 40 | options?: TransactionGetOptions, 41 | ): 42 | | Promise | TransactionGetOutput<[...E], undefined> | TransactGetItemsOutput> 43 | | TransactGetItemsCommandInput { 44 | const throwOnNotFound = options?.throwOnNotFound ?? true; 45 | const commandInput: TransactGetItemsCommandInput = { 46 | TransactItems: transactions.map((transaction) => ({ 47 | Get: transaction.get, 48 | })), 49 | ...options?.extraInput, 50 | }; 51 | 52 | if (options?.return === 'input') { 53 | return commandInput; 54 | } 55 | 56 | return (async () => { 57 | const result = await Dynamode.ddb.get().transactGetItems(commandInput); 58 | 59 | if (options?.return === 'output') { 60 | return result; 61 | } 62 | 63 | const responses = result.Responses || []; 64 | const items = responses.map((response) => response.Item); 65 | 66 | const entities = transactions.map((transaction, idx) => { 67 | const item = items[idx]; 68 | 69 | if (throwOnNotFound && !item) { 70 | throw new NotFoundError(); 71 | } 72 | 73 | if (item) { 74 | return convertAttributeValuesToEntity(transaction.entity, item); 75 | } 76 | }) as [...TransactionGetOutput['items']]; 77 | 78 | return { 79 | items: entities, 80 | count: entities.length, 81 | }; 82 | })(); 83 | } 84 | -------------------------------------------------------------------------------- /lib/transactionGet/types.ts: -------------------------------------------------------------------------------- 1 | import { Get, TransactGetItemsCommandInput } from '@aws-sdk/client-dynamodb'; 2 | import Entity from '@lib/entity'; 3 | import type { ReturnOption } from '@lib/entity/types'; 4 | 5 | export type TransactionGet = { 6 | get: Get; 7 | entity: E; 8 | }; 9 | 10 | export type TransactionGetInput> = { 11 | readonly [K in keyof E]: TransactionGet; 12 | }; 13 | 14 | export type TransactionGetOptions = { 15 | return?: ReturnOption; 16 | extraInput?: Partial; 17 | throwOnNotFound?: boolean; 18 | }; 19 | 20 | export type TransactionGetOutput, Extra = never> = { 21 | items: { [K in keyof E]: InstanceType | Extra }; 22 | count: number; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/transactionWrite/index.ts: -------------------------------------------------------------------------------- 1 | import { TransactWriteItemsCommandInput, TransactWriteItemsOutput } from '@aws-sdk/client-dynamodb'; 2 | import Dynamode from '@lib/dynamode/index'; 3 | import Entity from '@lib/entity'; 4 | import { convertAttributeValuesToEntity } from '@lib/entity/helpers/converters'; 5 | import type { 6 | TransactionWrite, 7 | TransactionWriteInput, 8 | TransactionWriteOptions, 9 | TransactionWriteOutput, 10 | } from '@lib/transactionWrite/types'; 11 | 12 | export default function transactionWrite>>( 13 | transactions: TransactionWriteInput<[...TW]>, 14 | options?: TransactionWriteOptions & { return?: 'default' }, 15 | ): Promise>; 16 | 17 | export default function transactionWrite>>( 18 | transactions: TransactionWriteInput<[...TW]>, 19 | options: TransactionWriteOptions & { return: 'output' }, 20 | ): Promise; 21 | 22 | export default function transactionWrite>>( 23 | transactions: TransactionWriteInput<[...TW]>, 24 | options: TransactionWriteOptions & { return: 'input' }, 25 | ): TransactWriteItemsCommandInput; 26 | 27 | export default function transactionWrite>>( 28 | transactions: TransactionWriteInput<[...TW]>, 29 | options?: TransactionWriteOptions, 30 | ): Promise | TransactWriteItemsOutput> | TransactWriteItemsCommandInput { 31 | const commandInput: TransactWriteItemsCommandInput = { 32 | TransactItems: transactions.map((transaction) => ({ 33 | Update: 'update' in transaction ? transaction.update : undefined, 34 | Put: 'put' in transaction ? transaction.put : undefined, 35 | ConditionCheck: 'condition' in transaction ? transaction.condition : undefined, 36 | Delete: 'delete' in transaction ? transaction.delete : undefined, 37 | })), 38 | ClientRequestToken: options?.idempotencyKey, 39 | ...options?.extraInput, 40 | }; 41 | 42 | if (options?.return === 'input') { 43 | return commandInput; 44 | } 45 | 46 | return (async () => { 47 | const result = await Dynamode.ddb.get().transactWriteItems(commandInput); 48 | 49 | if (options?.return === 'output') { 50 | return result; 51 | } 52 | 53 | const entities = transactions.map((transaction) => { 54 | const item = 'put' in transaction ? transaction.put?.Item : undefined; 55 | if (item) { 56 | return convertAttributeValuesToEntity(transaction.entity, item); 57 | } 58 | }) as [...TransactionWriteOutput['items']]; 59 | 60 | return { 61 | items: entities, 62 | count: entities.length, 63 | }; 64 | })(); 65 | } 66 | -------------------------------------------------------------------------------- /lib/transactionWrite/types.ts: -------------------------------------------------------------------------------- 1 | import { ConditionCheck, Delete, Put, TransactWriteItemsCommandInput, Update } from '@aws-sdk/client-dynamodb'; 2 | import Entity from '@lib/entity'; 3 | import type { ReturnOption } from '@lib/entity/types'; 4 | 5 | export type TransactionUpdate = { 6 | entity: E; 7 | update: Update; 8 | }; 9 | export type TransactionPut = { entity: E; put: Put }; 10 | export type TransactionDelete = { 11 | entity: E; 12 | delete: Delete; 13 | }; 14 | export type TransactionCondition = { 15 | entity: E; 16 | condition: ConditionCheck; 17 | }; 18 | export type TransactionWrite = 19 | | TransactionUpdate 20 | | TransactionPut 21 | | TransactionDelete 22 | | TransactionCondition; 23 | 24 | export type TransactionWriteInput>> = { 25 | readonly [K in keyof TW]: TW[K]; 26 | }; 27 | 28 | export type TransactionWriteOptions = { 29 | return?: ReturnOption; 30 | extraInput?: Partial; 31 | idempotencyKey?: string; 32 | }; 33 | 34 | export type TransactionWriteOutput>> = { 35 | items: { 36 | [K in keyof TW]: TW[K] extends TransactionPut ? InstanceType : undefined; 37 | }; 38 | count: number; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/utils/ExpressionBuilder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AttributeNames, 3 | AttributeValues, 4 | InvalidParameter, 5 | isNotEmpty, 6 | Operators, 7 | RESERVED_WORDS, 8 | splitListPathReference, 9 | valueToDynamo, 10 | } from '@lib/utils'; 11 | 12 | type ExpressionBuilderProps = { 13 | attributeNames?: AttributeNames; 14 | attributeValues?: AttributeValues; 15 | }; 16 | 17 | export class ExpressionBuilder { 18 | private _attributeNames: AttributeNames; 19 | private _attributeValues: AttributeValues; 20 | 21 | constructor(props?: ExpressionBuilderProps) { 22 | this._attributeNames = props?.attributeNames || {}; 23 | this._attributeValues = props?.attributeValues || {}; 24 | } 25 | 26 | get attributeNames() { 27 | return isNotEmpty(this._attributeNames) ? this._attributeNames : undefined; 28 | } 29 | 30 | get attributeValues() { 31 | return isNotEmpty(this._attributeValues) ? this._attributeValues : undefined; 32 | } 33 | 34 | public run(operators: Operators): string { 35 | return operators 36 | .map((operator) => { 37 | if ('expression' in operator) { 38 | return operator.expression; 39 | } 40 | 41 | if ('value' in operator) { 42 | return this.substituteValue(operator.key, operator.value); 43 | } 44 | 45 | if ('key' in operator) { 46 | return this.substituteName(operator.key); 47 | } 48 | }) 49 | .join(''); 50 | } 51 | 52 | public substituteName(key: string): string { 53 | return key 54 | .split('.') 55 | .map((key) => { 56 | const [keyPart, listReferencePart] = splitListPathReference(key); 57 | 58 | if (RESERVED_WORDS.has(keyPart.toUpperCase())) { 59 | const substituteKey = `#${keyPart}`; 60 | this._attributeNames[substituteKey] = keyPart; 61 | return substituteKey + listReferencePart; 62 | } 63 | 64 | return keyPart + listReferencePart; 65 | }) 66 | .join('.'); 67 | } 68 | 69 | public substituteValue(key: string, value: unknown): string { 70 | const valueName = key.replace(/\[/g, '_index').replace(/\]/g, '').split('.').join('_'); 71 | 72 | for (let i = 0; i < 1000; i++) { 73 | const suffix = i ? `__${i}` : ''; 74 | const substituteValueName = `:${valueName}${suffix}`; 75 | if (!this._attributeValues[substituteValueName]) { 76 | this._attributeValues[substituteValueName] = valueToDynamo(value); 77 | return substituteValueName; 78 | } 79 | } 80 | 81 | throw new InvalidParameter(`Couldn't substitute a value for key: ${key}. Value key out of range`); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/utils/converter.ts: -------------------------------------------------------------------------------- 1 | import { AttributeValue } from '@aws-sdk/client-dynamodb'; 2 | import ddbConverter from '@lib/dynamode/converter'; 3 | import { AttributeValues, GenericObject } from '@lib/utils'; 4 | 5 | export function objectToDynamo(object: GenericObject): AttributeValues { 6 | return ddbConverter.get().marshall(object, { removeUndefinedValues: true }); 7 | } 8 | 9 | export function valueToDynamo(value: any): AttributeValue { 10 | return ddbConverter.get().convertToAttr(value); 11 | } 12 | 13 | export function valueFromDynamo(value: AttributeValue): unknown { 14 | return ddbConverter.get().convertToNative(value); 15 | } 16 | 17 | export function fromDynamo(object: AttributeValues): GenericObject { 18 | return ddbConverter.get().unmarshall(object); 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export const createError = (defaultMessage: string, errorName: string) => 2 | class DynamodeError extends Error { 3 | name: string; 4 | message: string; 5 | 6 | constructor(message?: string) { 7 | super(); 8 | this.name = errorName; 9 | this.message = message || defaultMessage; 10 | Object.setPrototypeOf(this, DynamodeError.prototype); 11 | } 12 | }; 13 | 14 | export const NotFoundError = createError('Item not found', 'NotFoundError'); 15 | export const InvalidParameter = createError('Invalid Parameter', 'InvalidParameter'); 16 | export const ValidationError = createError('Validation failed', 'ValidationError'); 17 | export const ConflictError = createError('Conflict', 'ConflictError'); 18 | export const DynamodeStorageError = createError('Dynamode storage failed', 'DynamodeStorageError'); 19 | export const DynamodeStreamError = createError('Dynamode stream failed', 'DynamodeStreamError'); 20 | -------------------------------------------------------------------------------- /lib/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export function duplicatesInArray(arr: Array): boolean { 2 | return new Set(arr).size !== arr.length; 3 | } 4 | 5 | export function isEmpty(obj: object): boolean { 6 | return Object.keys(obj).length === 0; 7 | } 8 | 9 | export function isNotEmpty(obj?: object): obj is object { 10 | if (!obj) return false; 11 | return !isEmpty(obj); 12 | } 13 | 14 | export function isNotEmptyString(str: string) { 15 | if (str === '') return false; 16 | return true; 17 | } 18 | 19 | export function isNotEmptyArray(array?: Array): array is Array { 20 | return !!array && array.length > 0; 21 | } 22 | 23 | export function insertBetween(arr: T[], separator: T | T[]): T[] { 24 | return arr.flatMap((value, index, array) => 25 | array.length - 1 !== index // check for the last item 26 | ? Array.isArray(separator) 27 | ? [value, ...separator] 28 | : [value, separator] 29 | : value, 30 | ); 31 | } 32 | 33 | export function deepEqual(obj1: any, obj2: any): boolean { 34 | // Check if the objects are strictly equal 35 | if (obj1 === obj2) { 36 | return true; 37 | } 38 | 39 | // Check if both objects are objects and not null 40 | if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) { 41 | return false; 42 | } 43 | 44 | // Get the keys of the objects 45 | const keys1 = Object.keys(obj1); 46 | const keys2 = Object.keys(obj2); 47 | 48 | // Check if the objects have the same number of properties 49 | if (keys1.length !== keys2.length) { 50 | return false; 51 | } 52 | 53 | // Check if all properties are deep equal 54 | for (const key of keys1) { 55 | if (!deepEqual(obj1[key], obj2[key])) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | /** Splits key that reference a list element. 64 | * Alphanumeric part can be replaced in case it is a DynamoDB reserved word. 65 | * Examples: 'list[0][1]' -> ['list', '[0][1]']. 'auto[10]' -> ['auto', '[10]'] */ 66 | export function splitListPathReference(key: string): [string, string] { 67 | const index = key.search(/\[\d+\]/g); 68 | 69 | if (index === -1) { 70 | return [key, '']; 71 | } 72 | 73 | return [key.slice(0, index), key.slice(index)]; 74 | } 75 | 76 | /** 77 | * Performs a deep merge of objects and returns new object. Does not modify 78 | * objects (immutable) and merges arrays via concatenation. 79 | * 80 | * @param {...object} objects - Objects to merge 81 | * @returns {object} New object with merged key/values 82 | */ 83 | export function mergeObjects>(...objects: T[]): T { 84 | const isObject = (obj: unknown): obj is T => !!obj && typeof obj === 'object' && !Array.isArray(obj); 85 | 86 | return objects.reduce((prev, obj) => { 87 | Object.keys(obj).forEach((key: keyof T) => { 88 | const prevValue = prev[key]; 89 | const value = obj[key]; 90 | 91 | if (isObject(prevValue) && isObject(value)) { 92 | prev[key] = mergeObjects(prevValue, value); 93 | } else { 94 | prev[key] = value; 95 | } 96 | }); 97 | 98 | return prev; 99 | }, {} as T); 100 | } 101 | 102 | export async function timeout(ms: number): Promise { 103 | if (ms > 0) { 104 | return new Promise((resolve) => setTimeout(resolve, ms)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@lib/utils/ExpressionBuilder'; 2 | export * from '@lib/utils/converter'; 3 | export * from '@lib/utils/errors'; 4 | export * from '@lib/utils/helpers'; 5 | export * from '@lib/utils/types'; 6 | export * from '@lib/utils/constants'; 7 | -------------------------------------------------------------------------------- /lib/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeValue } from '@aws-sdk/client-dynamodb'; 2 | 3 | // Common 4 | 5 | export type AttributeValues = Record; 6 | export type AttributeNames = Record; 7 | export type GenericObject = Record; 8 | 9 | // Flatten entity 10 | export type FlattenObject = CollapseEntries>; 11 | 12 | type Entry = { key: string; value: unknown }; 13 | type EmptyEntry = { key: ''; value: TValue }; 14 | type ExcludedTypes = Date | Set | Map | Uint8Array; 15 | type ArrayEncoder = `[${bigint}]`; 16 | 17 | type EscapeArrayKey = TKey extends `${infer TKeyBefore}.${ArrayEncoder}${infer TKeyAfter}` 18 | ? EscapeArrayKey<`${TKeyBefore}${ArrayEncoder}${TKeyAfter}`> 19 | : TKey; 20 | 21 | // Transforms entries to one flattened type 22 | type CollapseEntries = { 23 | [E in TEntry as EscapeArrayKey]: E['value']; 24 | }; 25 | 26 | // Transforms array type to object 27 | type CreateArrayEntry = OmitItself< 28 | TValue extends unknown[] ? { [k: ArrayEncoder]: TValue[number] } : TValue, 29 | TValueInitial 30 | >; 31 | 32 | // Omit the type that references itself 33 | type OmitItself = TValue extends TValueInitial 34 | ? EmptyEntry 35 | : OmitExcludedTypes; 36 | 37 | // Omit the type that is listed in ExcludedTypes union 38 | type OmitExcludedTypes = TValue extends ExcludedTypes 39 | ? EmptyEntry 40 | : CreateObjectEntries; 41 | 42 | type CreateObjectEntries = TValue extends object 43 | ? { 44 | // Checks that Key is of type string 45 | [TKey in keyof TValue]-?: TKey extends string 46 | ? // Nested key can be an object, run recursively to the bottom 47 | CreateArrayEntry extends infer TNestedValue 48 | ? TNestedValue extends Entry 49 | ? TNestedValue['key'] extends '' 50 | ? { 51 | key: TKey; 52 | value: TNestedValue['value']; 53 | } 54 | : 55 | | { 56 | key: `${TKey}.${TNestedValue['key']}`; 57 | value: TNestedValue['value']; 58 | } 59 | | { 60 | key: TKey; 61 | value: TValue[TKey]; 62 | } 63 | : never 64 | : never 65 | : never; 66 | }[keyof TValue] // Builds entry for each key 67 | : EmptyEntry; 68 | 69 | // Narrow utility 70 | 71 | type Narrowable = string | number | bigint | boolean; 72 | export type Narrow = 73 | | (T extends Narrowable ? T : never) 74 | | { 75 | [K in keyof T]: Narrow; 76 | }; 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamode", 3 | "version": "1.5.0", 4 | "description": "Dynamode is a modeling tool for Amazon's DynamoDB", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "build": "tsc && tsc-alias", 9 | "typecheck": "tsc --project tsconfig.typecheck.json", 10 | "postbuild": "cp package.json dist && cp README.md dist && cp LICENSE dist", 11 | "build:watch": "concurrently --kill-others \"tsc -w\" \"tsc-alias -w\"", 12 | "test": "vitest run unit", 13 | "test:watch": "vitest unit", 14 | "test:e2e": "vitest run e2e --no-file-parallelism", 15 | "test:ui": "vitest unit --ui", 16 | "test:types": "vitest run types --typecheck", 17 | "coverage": "vitest run unit --coverage", 18 | "lint": "eslint lib --ext .ts,.js --max-warnings 0", 19 | "lint:fix": "npm run lint -- --fix", 20 | "bump:patch": "npm version patch", 21 | "bump:minor": "npm version minor", 22 | "bump:major": "npm version major", 23 | "bump:next": "npm version prerelease --preid rc", 24 | "publish:next": "npm run build && cd dist && npm publish --tag next" 25 | }, 26 | "dependencies": { 27 | "@aws-sdk/client-dynamodb": "^3.474.0", 28 | "@aws-sdk/util-dynamodb": "^3.474.0" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20.2.1", 32 | "@typescript-eslint/eslint-plugin": "^5.59.6", 33 | "@typescript-eslint/parser": "^5.59.6", 34 | "@vitest/coverage-v8": "^1.1.3", 35 | "@vitest/ui": "^1.1.3", 36 | "c8": "^7.13.0", 37 | "concurrently": "^8.0.1", 38 | "eslint": "^8.40.0", 39 | "eslint-config-prettier": "^8.8.0", 40 | "eslint-import-resolver-typescript": "^3.5.5", 41 | "eslint-plugin-import": "^2.27.5", 42 | "eslint-plugin-prettier": "^4.2.1", 43 | "eslint-plugin-simple-import-sort": "^10.0.0", 44 | "eslint-plugin-unused-imports": "^2.0.0", 45 | "prettier": "^2.8.8", 46 | "tsc-alias": "^1.8.6", 47 | "type-fest": "^3.10.0", 48 | "typescript": "^5.0.4", 49 | "vite-tsconfig-paths": "^4.2.0", 50 | "vitest": "^1.1.3" 51 | }, 52 | "peerDependencies": { 53 | "typescript": ">=4.0.0" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/blazejkustra/dynamode.git" 58 | }, 59 | "author": { 60 | "name": "Błażej Kustra", 61 | "email": "kustra.blazej@gmail.com" 62 | }, 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/blazejkustra/dynamode/issues" 66 | }, 67 | "homepage": "https://github.com/blazejkustra/dynamode#readme", 68 | "keywords": [ 69 | "dynamodb", 70 | "dynamo", 71 | "aws", 72 | "amazon", 73 | "document", 74 | "model", 75 | "entity", 76 | "schema", 77 | "database", 78 | "data", 79 | "datastore", 80 | "nosql", 81 | "db", 82 | "odm" 83 | ], 84 | "engines": { 85 | "node": ">=16.0.0", 86 | "npm": ">=8" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/e2e/entity/batchDelete.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { NotFoundError } from '@lib/utils'; 4 | 5 | import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; 6 | import { mockEntityFactory } from '../mockEntityFactory'; 7 | 8 | describe('EntityManager.batchDelete', () => { 9 | beforeAll(async () => { 10 | vi.useFakeTimers(); 11 | await TestTableManager.createTable(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await TestTableManager.deleteTable(TEST_TABLE_NAME); 16 | vi.useRealTimers(); 17 | vi.restoreAllMocks(); 18 | }); 19 | 20 | describe.sequential('MockEntityManager', () => { 21 | test('Should return empty array if an empty array is passed', async () => { 22 | // Act 23 | const { unprocessedItems } = await MockEntityManager.batchDelete([]); 24 | 25 | // Assert 26 | expect(unprocessedItems).toEqual([]); 27 | }); 28 | 29 | test('Should be able to delete multiple items', async () => { 30 | // Arrange 31 | const mock1 = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 32 | const mock2 = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2' }); 33 | const mock3 = mockEntityFactory({ partitionKey: 'PK3', sortKey: 'SK3' }); 34 | await MockEntityManager.put(mock1); 35 | await MockEntityManager.put(mock2); 36 | await MockEntityManager.put(mock3); 37 | 38 | // Act 39 | await MockEntityManager.batchDelete([ 40 | { partitionKey: 'PK1', sortKey: 'SK1' }, 41 | { partitionKey: 'PK2', sortKey: 'SK2' }, 42 | { partitionKey: 'PK3', sortKey: 'SK3' }, 43 | ]); 44 | 45 | // Assert 46 | await expect(MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' })).rejects.toThrow(NotFoundError); 47 | await expect(MockEntityManager.get({ partitionKey: 'PK2', sortKey: 'SK2' })).rejects.toThrow(NotFoundError); 48 | await expect(MockEntityManager.get({ partitionKey: 'PK3', sortKey: 'SK3' })).rejects.toThrow(NotFoundError); 49 | }); 50 | 51 | test('Should not be able to delete duplicated items', async () => { 52 | // Arrange 53 | const mock1 = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 54 | const mock2 = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2' }); 55 | await MockEntityManager.put(mock1); 56 | await MockEntityManager.put(mock2); 57 | 58 | // Act & Assert 59 | await expect( 60 | MockEntityManager.batchDelete([ 61 | { partitionKey: 'PK1', sortKey: 'SK1' }, 62 | { partitionKey: 'PK1', sortKey: 'SK1' }, 63 | ]), 64 | ).rejects.toThrow('Provided list of item keys contains duplicates'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/e2e/entity/batchPut.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; 4 | import { mockEntityFactory } from '../mockEntityFactory'; 5 | 6 | describe('EntityManager.batchPut', () => { 7 | beforeAll(async () => { 8 | vi.useFakeTimers(); 9 | await TestTableManager.createTable(); 10 | }); 11 | 12 | afterAll(async () => { 13 | await TestTableManager.deleteTable(TEST_TABLE_NAME); 14 | vi.useRealTimers(); 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe.sequential('MockEntityManager', () => { 19 | test('Should return empty arrays if an empty array is passed', async () => { 20 | // Act 21 | const { items: mocks, unprocessedItems } = await MockEntityManager.batchPut([]); 22 | 23 | // Assert 24 | expect(mocks).toEqual([]); 25 | expect(unprocessedItems).toEqual([]); 26 | }); 27 | 28 | test('Should be able to retrieve multiple items', async () => { 29 | // Arrange 30 | const mock1 = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 31 | const mock2 = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2' }); 32 | const mock3 = mockEntityFactory({ partitionKey: 'PK3', sortKey: 'SK3' }); 33 | 34 | // Act 35 | await MockEntityManager.batchPut([mock1, mock2, mock3]); 36 | 37 | // Assert 38 | const retrievedMock1 = await MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' }); 39 | const retrievedMock2 = await MockEntityManager.get({ partitionKey: 'PK2', sortKey: 'SK2' }); 40 | const retrievedMock3 = await MockEntityManager.get({ partitionKey: 'PK3', sortKey: 'SK3' }); 41 | 42 | expect(retrievedMock1).toEqual(mock1); 43 | expect(retrievedMock2).toEqual(mock2); 44 | expect(retrievedMock3).toEqual(mock3); 45 | }); 46 | 47 | test('Should be able to put duplicates', async () => { 48 | // Arrange 49 | const mock1 = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 50 | const mock2 = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2' }); 51 | 52 | // Act & Assert 53 | await expect(MockEntityManager.batchPut([mock1, mock2, mock2])).rejects.toThrow( 54 | 'Provided list of item keys contains duplicates', 55 | ); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/e2e/entity/create.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; 4 | import { mockEntityFactory } from '../mockEntityFactory'; 5 | 6 | describe.sequential('EntityManager.create', () => { 7 | beforeAll(async () => { 8 | vi.useFakeTimers(); 9 | await TestTableManager.createTable(); 10 | }); 11 | 12 | afterAll(async () => { 13 | await TestTableManager.deleteTable(TEST_TABLE_NAME); 14 | vi.useRealTimers(); 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe.sequential('MockEntityManager', () => { 19 | test('Should be able to create an item', async () => { 20 | // Arrange 21 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 22 | 23 | // Act 24 | await MockEntityManager.create(mock); 25 | 26 | // Assert 27 | const mockEntityRetrieved = await MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' }); 28 | expect(mockEntityRetrieved).toEqual(mock); 29 | }); 30 | 31 | test('Should fail to create the same item by default', async () => { 32 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 33 | await expect(MockEntityManager.create(mock)).rejects.toThrow('The conditional request failed'); 34 | }); 35 | 36 | test('Should be able to overwrite an item by with extra option', async () => { 37 | // Arrange 38 | const mock = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2', string: 'before' }); 39 | const mockOverwrite = mockEntityFactory({ partitionKey: 'PK2', sortKey: 'SK2', string: 'after' }); 40 | 41 | // Act 42 | await MockEntityManager.create(mock); 43 | await MockEntityManager.create(mockOverwrite, { overwrite: true }); 44 | 45 | // Assert 46 | const mockEntityRetrieved = await MockEntityManager.get({ partitionKey: 'PK2', sortKey: 'SK2' }); 47 | expect(mockEntityRetrieved).toEqual(mockOverwrite); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/e2e/entity/delete.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { NotFoundError } from '@lib/utils'; 4 | 5 | import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; 6 | import { mockEntityFactory } from '../mockEntityFactory'; 7 | 8 | describe('EntityManager.delete', () => { 9 | beforeAll(async () => { 10 | vi.useFakeTimers(); 11 | await TestTableManager.createTable(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await TestTableManager.deleteTable(TEST_TABLE_NAME); 16 | vi.useRealTimers(); 17 | vi.restoreAllMocks(); 18 | }); 19 | 20 | describe.sequential('MockEntityManager', () => { 21 | test('Should be able to delete an item', async () => { 22 | // Arrange 23 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 24 | await MockEntityManager.put(mock); 25 | 26 | // Act 27 | await MockEntityManager.delete({ partitionKey: 'PK1', sortKey: 'SK1' }); 28 | 29 | // Assert 30 | await expect(MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' })).rejects.toThrow(NotFoundError); 31 | }); 32 | 33 | test('Should be able to delete not existing item', async () => { 34 | // Act 35 | await MockEntityManager.delete({ partitionKey: 'PK1', sortKey: 'SK1' }); 36 | 37 | // Assert 38 | await expect(MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' })).rejects.toThrow(NotFoundError); 39 | }); 40 | 41 | test('Should throw an error if deleting not existing item with extra option', async () => { 42 | // Act & Assert 43 | await expect( 44 | MockEntityManager.delete({ partitionKey: 'PK1', sortKey: 'SK1' }, { throwErrorIfNotExists: true }), 45 | ).rejects.toThrow('The conditional request failed'); 46 | }); 47 | 48 | test('Should throw an error if condition is not met', async () => { 49 | // Arrange 50 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 51 | await MockEntityManager.put(mock); 52 | 53 | // Act & Assert 54 | await expect( 55 | MockEntityManager.delete( 56 | { partitionKey: 'PK1', sortKey: 'SK1' }, 57 | { condition: MockEntityManager.condition().attribute('number').not().ge(0) }, 58 | ), 59 | ).rejects.toThrow('The conditional request failed'); 60 | }); 61 | 62 | test('Should return old values of the item', async () => { 63 | // Arrange 64 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 65 | await MockEntityManager.put(mock); 66 | 67 | // Act 68 | const oldMock = await MockEntityManager.delete({ partitionKey: 'PK1', sortKey: 'SK1' }); 69 | 70 | // Act & Assert 71 | expect(oldMock).toEqual(mock); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/e2e/entity/get.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { NotFoundError } from '@lib/utils'; 4 | 5 | import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; 6 | import { mockEntityFactory } from '../mockEntityFactory'; 7 | 8 | describe('EntityManager.get', () => { 9 | beforeAll(async () => { 10 | vi.useFakeTimers(); 11 | await TestTableManager.createTable(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await TestTableManager.deleteTable(TEST_TABLE_NAME); 16 | vi.useRealTimers(); 17 | vi.restoreAllMocks(); 18 | }); 19 | 20 | describe.sequential('MockEntityManager', () => { 21 | test('Should be able to retrieve an item', async () => { 22 | // Arrange 23 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 24 | await MockEntityManager.put(mock); 25 | 26 | // Act 27 | const mockEntityRetrieved = await MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' }); 28 | 29 | // Assert 30 | expect(mockEntityRetrieved).toEqual(mock); 31 | }); 32 | 33 | test('Should throw an error if item is not found', async () => { 34 | // Arrange 35 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 36 | const mockEntityRetrieved = await MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' }); 37 | expect(mockEntityRetrieved).toEqual(mock); 38 | 39 | // Act 40 | await MockEntityManager.delete({ partitionKey: 'PK1', sortKey: 'SK1' }); 41 | 42 | // Assert 43 | await expect(MockEntityManager.get({ partitionKey: 'PK1', sortKey: 'SK1' })).rejects.toThrow(NotFoundError); 44 | }); 45 | 46 | test('Should retrieve only some of the attributes', async () => { 47 | // Arrange 48 | const mock = mockEntityFactory({ partitionKey: 'PK1', sortKey: 'SK1' }); 49 | await MockEntityManager.put(mock); 50 | 51 | // Act 52 | const mockEntityRetrieved = await MockEntityManager.get( 53 | { partitionKey: 'PK1', sortKey: 'SK1' }, 54 | { attributes: ['string', 'number'] }, 55 | ); 56 | 57 | // Assert 58 | expect(mockEntityRetrieved.string).toEqual('string'); 59 | expect(mockEntityRetrieved.number).toEqual(1); 60 | expect(mockEntityRetrieved.boolean).toEqual(undefined); 61 | expect(mockEntityRetrieved.object).toEqual(undefined); 62 | expect(mockEntityRetrieved.partitionKey).toEqual(undefined); 63 | expect(mockEntityRetrieved.sortKey).toEqual(undefined); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/e2e/mockEntityFactory.ts: -------------------------------------------------------------------------------- 1 | import { MockEntity, MockEntityProps } from '../fixtures/TestTable'; 2 | 3 | export function mockEntityFactory(props?: Partial): MockEntity { 4 | return new MockEntity({ 5 | partitionKey: 'PK', 6 | sortKey: 'SK', 7 | string: 'string', 8 | number: 1, 9 | object: { 10 | required: 2, 11 | }, 12 | map: new Map([['1', '2']]), 13 | set: new Set(['1', '2', '3']), 14 | array: ['1', '2'], 15 | boolean: true, 16 | binary: new Uint8Array([1, 2, 3]), 17 | ...props, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | "target": "es2020", 6 | "lib": ["es2020", "DOM"], 7 | "experimentalDecorators": true, 8 | "rootDir": ".." 9 | }, 10 | "include": ["../lib", "."], 11 | "exclude": [] 12 | } -------------------------------------------------------------------------------- /tests/types/EntityKey.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, test } from 'vitest'; 2 | 3 | import Entity from '@lib/entity'; 4 | import { EntityKey } from '@lib/entity/types'; 5 | 6 | import { MockEntity } from '../fixtures/TestTable'; 7 | 8 | type BaseKeys = 'dynamodeEntity'; 9 | 10 | type MockEntityDeepKeys = 11 | | BaseKeys 12 | | 'string' 13 | | 'object' 14 | | 'object.optional' 15 | | 'object.required' 16 | | 'object.nestedArray' 17 | | `object.nestedArray[${bigint}]` 18 | | `object.nestedArray[${bigint}].date` 19 | | 'array' 20 | | `array[${bigint}]` 21 | | 'map' 22 | | 'set' 23 | | 'number' 24 | | 'boolean' 25 | | 'strDate' 26 | | 'numDate' 27 | | 'binary' 28 | | 'unsaved' 29 | | 'partitionKey' 30 | | 'sortKey' 31 | | 'GSI_1_PK' 32 | | 'GSI_2_PK' 33 | | 'GSI_SK' 34 | | 'GSI_3_SK' 35 | | 'LSI_1_SK' 36 | | 'createdAt' 37 | | 'updatedAt'; 38 | 39 | class ArrayEntity extends Entity { 40 | array!: Array; 41 | } 42 | 43 | type ArrayKeys = BaseKeys | 'array' | `array[${bigint}]`; 44 | 45 | class MapEntity extends Entity { 46 | map!: Map; 47 | } 48 | 49 | type MapKeys = BaseKeys | 'map'; 50 | 51 | class ObjectEntity extends Entity { 52 | object!: { 53 | optional?: string; 54 | required: string; 55 | deep: { 56 | optional?: string; 57 | required: string; 58 | }; 59 | }; 60 | } 61 | 62 | type ObjectKeys = 63 | | BaseKeys 64 | | 'object' 65 | | 'object.optional' 66 | | 'object.required' 67 | | 'object.deep' 68 | | 'object.deep.optional' 69 | | 'object.deep.required'; 70 | 71 | class RecordEntity extends Entity { 72 | record!: Record; 73 | } 74 | 75 | type RecordKeys = BaseKeys | 'record' | `record.${string}`; 76 | 77 | class RecurringEntity extends Entity { 78 | entity!: RecurringEntity; 79 | } 80 | 81 | type RecurringKeys = BaseKeys | 'entity'; 82 | 83 | describe('EntityKey type tests', () => { 84 | test('Should return deep keys of all attributes', async () => { 85 | expectTypeOf>().toEqualTypeOf(); 86 | expectTypeOf>().toEqualTypeOf(); 87 | expectTypeOf>().toEqualTypeOf(); 88 | expectTypeOf>().toEqualTypeOf(); 89 | expectTypeOf>().toEqualTypeOf(); 90 | expectTypeOf>().toEqualTypeOf(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/customName.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import customName from '@lib/decorators/helpers/customName'; 4 | import Dynamode from '@lib/dynamode/index'; 5 | 6 | describe('Decorators', () => { 7 | let transferMetadataSpy = vi.spyOn(Dynamode.storage, 'transferMetadata'); 8 | 9 | beforeEach(() => { 10 | transferMetadataSpy = vi.spyOn(Dynamode.storage, 'transferMetadata'); 11 | transferMetadataSpy.mockReturnValue(undefined); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe('customName', async () => { 19 | test('Should call transfer metadata when renaming an entity', async () => { 20 | customName('NEW_NAME')(class OLD_NAME {}); 21 | expect(transferMetadataSpy).toHaveBeenNthCalledWith(1, 'OLD_NAME', 'NEW_NAME'); 22 | }); 23 | 24 | test('Should call transfer metadata when renaming an entity with the same name', async () => { 25 | customName('OLD_NAME')(class OLD_NAME {}); 26 | expect(transferMetadataSpy).toHaveBeenNthCalledWith(1, 'OLD_NAME', 'OLD_NAME'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/dates.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { numberDate, stringDate } from '@lib/decorators/helpers/dates'; 4 | import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute'; 5 | 6 | describe('Decorators', () => { 7 | let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 8 | 9 | beforeEach(() => { 10 | decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 11 | decorateAttributeSpy.mockReturnValue(() => undefined); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe('stringDate', async () => { 19 | test('Should call decorateAttribute with String attribute type + options', async () => { 20 | stringDate(); 21 | stringDate({ prefix: 'PREFIX', suffix: 'SUFFIX' }); 22 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'date', undefined); 23 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'date', { prefix: 'PREFIX', suffix: 'SUFFIX' }); 24 | }); 25 | }); 26 | 27 | describe('numberDate', async () => { 28 | test('Should call decorateAttribute with Number attribute type', async () => { 29 | numberDate(); 30 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'date'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/decorateAttribute.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; 4 | import Dynamode from '@lib/dynamode/index'; 5 | 6 | import { MockEntity, mockInstance } from '../../../fixtures/TestTable'; 7 | 8 | describe('Decorators', () => { 9 | let registerAttributeSpy = vi.spyOn(Dynamode.storage, 'registerAttribute'); 10 | let registerIndexSpy = vi.spyOn(Dynamode.storage, 'registerIndex'); 11 | 12 | beforeEach(() => { 13 | registerAttributeSpy = vi.spyOn(Dynamode.storage, 'registerAttribute'); 14 | registerAttributeSpy.mockReturnValue(undefined); 15 | 16 | registerIndexSpy = vi.spyOn(Dynamode.storage, 'registerIndex'); 17 | registerIndexSpy.mockReturnValue(undefined); 18 | }); 19 | 20 | afterEach(() => { 21 | vi.restoreAllMocks(); 22 | }); 23 | 24 | describe('decorateAttribute', async () => { 25 | test('Should call storage registerAttribute method', async () => { 26 | decorateAttribute(String, 'attribute')(mockInstance, 'string'); 27 | 28 | expect(registerAttributeSpy).toBeCalledWith(MockEntity.name, 'string', { 29 | propertyName: 'string', 30 | type: String, 31 | role: 'attribute', 32 | }); 33 | }); 34 | 35 | test('Should call storage registerAttribute method with additional options', async () => { 36 | decorateAttribute(String, 'gsiPartitionKey', { 37 | prefix: 'PREFIX', 38 | suffix: 'SUFFIX', 39 | indexName: 'GSI', 40 | })(mockInstance, 'gsiKey'); 41 | 42 | expect(registerIndexSpy).toBeCalledWith(MockEntity.name, 'gsiKey', { 43 | propertyName: 'gsiKey', 44 | type: String, 45 | role: 'index', 46 | indexes: [{ name: 'GSI', role: 'gsiPartitionKey' }], 47 | prefix: 'PREFIX', 48 | suffix: 'SUFFIX', 49 | }); 50 | }); 51 | 52 | test('Should call storage registerAttribute method with additional options', async () => { 53 | expect(() => 54 | decorateAttribute(String, 'gsiPartitionKey', { 55 | prefix: 'PREFIX', 56 | suffix: 'SUFFIX', 57 | })(mockInstance, 'gsiKey'), 58 | ).toThrowError('Index name is required for gsiPartitionKey attribute'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/gsi.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute'; 4 | import { 5 | numberGsiPartitionKey, 6 | numberGsiSortKey, 7 | stringGsiPartitionKey, 8 | stringGsiSortKey, 9 | } from '@lib/decorators/helpers/gsi'; 10 | 11 | describe('Decorators', () => { 12 | let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 13 | 14 | beforeEach(() => { 15 | decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 16 | decorateAttributeSpy.mockReturnValue(() => undefined); 17 | }); 18 | 19 | afterEach(() => { 20 | vi.restoreAllMocks(); 21 | }); 22 | 23 | describe('stringGsiPartitionKey', async () => { 24 | test('Should call decorateAttribute with String attribute type + options', async () => { 25 | stringGsiPartitionKey({ indexName: 'GSI' }); 26 | stringGsiPartitionKey({ indexName: 'GSI', prefix: 'PREFIX', suffix: 'SUFFIX' }); 27 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'gsiPartitionKey', { indexName: 'GSI' }); 28 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'gsiPartitionKey', { 29 | indexName: 'GSI', 30 | prefix: 'PREFIX', 31 | suffix: 'SUFFIX', 32 | }); 33 | }); 34 | }); 35 | 36 | describe('numberGsiPartitionKey', async () => { 37 | test('Should call decorateAttribute with Number attribute type', async () => { 38 | numberGsiPartitionKey({ indexName: 'GSI' }); 39 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'gsiPartitionKey', { indexName: 'GSI' }); 40 | }); 41 | }); 42 | 43 | describe('stringGsiSortKey', async () => { 44 | test('Should call decorateAttribute with String attribute type + options', async () => { 45 | stringGsiSortKey({ indexName: 'GSI' }); 46 | stringGsiSortKey({ indexName: 'GSI', prefix: 'PREFIX', suffix: 'SUFFIX' }); 47 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'gsiSortKey', { indexName: 'GSI' }); 48 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'gsiSortKey', { 49 | indexName: 'GSI', 50 | prefix: 'PREFIX', 51 | suffix: 'SUFFIX', 52 | }); 53 | }); 54 | }); 55 | 56 | describe('numberGsiSortKey', async () => { 57 | test('Should call decorateAttribute with Number attribute type', async () => { 58 | numberGsiSortKey({ indexName: 'GSI' }); 59 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'gsiSortKey', { indexName: 'GSI' }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/lsi.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute'; 4 | import { numberLsiSortKey, stringLsiSortKey } from '@lib/decorators/helpers/lsi'; 5 | 6 | describe('Decorators', () => { 7 | let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 8 | 9 | beforeEach(() => { 10 | decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 11 | decorateAttributeSpy.mockReturnValue(() => undefined); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe('stringLsiSortKey', async () => { 19 | test('Should call decorateAttribute with String attribute type + options', async () => { 20 | stringLsiSortKey({ indexName: 'GSI' }); 21 | stringLsiSortKey({ indexName: 'GSI', prefix: 'PREFIX', suffix: 'SUFFIX' }); 22 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'lsiSortKey', { indexName: 'GSI' }); 23 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'lsiSortKey', { 24 | indexName: 'GSI', 25 | prefix: 'PREFIX', 26 | suffix: 'SUFFIX', 27 | }); 28 | }); 29 | }); 30 | 31 | describe('numberLsiSortKey', async () => { 32 | test('Should call decorateAttribute with Number attribute type', async () => { 33 | numberLsiSortKey({ indexName: 'GSI' }); 34 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'lsiSortKey', { indexName: 'GSI' }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/other.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute'; 4 | import { array, binary, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other'; 5 | 6 | describe('Decorators', () => { 7 | let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 8 | 9 | beforeEach(() => { 10 | decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 11 | decorateAttributeSpy.mockReturnValue(() => undefined); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | describe('string', async () => { 19 | test('Should call decorateAttribute with String attribute type + options', async () => { 20 | string(); 21 | string({ prefix: 'PREFIX', suffix: 'SUFFIX' }); 22 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'attribute', undefined); 23 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'attribute', { 24 | prefix: 'PREFIX', 25 | suffix: 'SUFFIX', 26 | }); 27 | }); 28 | }); 29 | 30 | describe('number', async () => { 31 | test('Should call decorateAttribute with Number attribute type', async () => { 32 | number(); 33 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'attribute'); 34 | }); 35 | }); 36 | 37 | describe('boolean', async () => { 38 | test('Should call decorateAttribute with Boolean attribute type', async () => { 39 | boolean(); 40 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Boolean, 'attribute'); 41 | }); 42 | }); 43 | 44 | describe('object', async () => { 45 | test('Should call decorateAttribute with Object attribute type', async () => { 46 | object(); 47 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Object, 'attribute'); 48 | }); 49 | }); 50 | 51 | describe('array', async () => { 52 | test('Should call decorateAttribute with Array attribute type', async () => { 53 | array(); 54 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Array, 'attribute'); 55 | }); 56 | }); 57 | 58 | describe('set', async () => { 59 | test('Should call decorateAttribute with Set attribute type', async () => { 60 | set(); 61 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Set, 'attribute'); 62 | }); 63 | }); 64 | 65 | describe('map', async () => { 66 | test('Should call decorateAttribute with Map attribute type', async () => { 67 | map(); 68 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Map, 'attribute'); 69 | }); 70 | }); 71 | 72 | describe('binary', async () => { 73 | test('Should call decorateAttribute with Uint8Array attribute type', async () => { 74 | binary(); 75 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Uint8Array, 'attribute'); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/prefixSuffix.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | 3 | import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix'; 4 | import Dynamode from '@lib/dynamode/index'; 5 | 6 | import { MockEntity, mockInstance } from '../../../fixtures/TestTable'; 7 | 8 | describe('Decorators', () => { 9 | describe('prefix', async () => { 10 | test('Should call storage updateAttributePrefix method', async () => { 11 | const updateAttributePrefixSpy = vi.spyOn(Dynamode.storage, 'updateAttributePrefix'); 12 | updateAttributePrefixSpy.mockReturnValue(undefined); 13 | prefix('PREFIX')(mockInstance, 'string'); 14 | expect(updateAttributePrefixSpy).toBeCalledWith(MockEntity.name, 'string', 'PREFIX'); 15 | }); 16 | }); 17 | 18 | describe('suffix', async () => { 19 | test('Should call storage updateAttributeSuffix method', async () => { 20 | const updateAttributeSuffixSpy = vi.spyOn(Dynamode.storage, 'updateAttributeSuffix'); 21 | updateAttributeSuffixSpy.mockReturnValue(undefined); 22 | suffix('SUFFIX')(mockInstance, 'string'); 23 | expect(updateAttributeSuffixSpy).toBeCalledWith(MockEntity.name, 'string', 'SUFFIX'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/unit/decorators/helpers/primaryKey.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute'; 4 | import { 5 | numberPartitionKey, 6 | numberSortKey, 7 | stringPartitionKey, 8 | stringSortKey, 9 | } from '@lib/decorators/helpers/primaryKey'; 10 | 11 | describe('Decorators', () => { 12 | let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 13 | 14 | beforeEach(() => { 15 | decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute'); 16 | decorateAttributeSpy.mockReturnValue(() => undefined); 17 | }); 18 | 19 | afterEach(() => { 20 | vi.restoreAllMocks(); 21 | }); 22 | 23 | describe('stringPartitionKey', async () => { 24 | test('Should call decorateAttribute with String attribute type + options', async () => { 25 | stringPartitionKey(); 26 | stringPartitionKey({ prefix: 'PREFIX', suffix: 'SUFFIX' }); 27 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'partitionKey', undefined); 28 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'partitionKey', { 29 | prefix: 'PREFIX', 30 | suffix: 'SUFFIX', 31 | }); 32 | }); 33 | }); 34 | 35 | describe('numberPartitionKey', async () => { 36 | test('Should call decorateAttribute with Number attribute type', async () => { 37 | numberPartitionKey(); 38 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'partitionKey'); 39 | }); 40 | }); 41 | 42 | describe('stringSortKey', async () => { 43 | test('Should call decorateAttribute with String attribute type + options', async () => { 44 | stringSortKey(); 45 | stringSortKey({ prefix: 'PREFIX', suffix: 'SUFFIX' }); 46 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, String, 'sortKey', undefined); 47 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(2, String, 'sortKey', { 48 | prefix: 'PREFIX', 49 | suffix: 'SUFFIX', 50 | }); 51 | }); 52 | }); 53 | 54 | describe('numberSortKey', async () => { 55 | test('Should call decorateAttribute with Number attribute type', async () => { 56 | numberSortKey(); 57 | expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Number, 'sortKey'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/unit/decorators/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import attribute from '@lib/decorators'; 4 | import { numberDate, stringDate } from '@lib/decorators/helpers/dates'; 5 | import { 6 | numberGsiPartitionKey, 7 | numberGsiSortKey, 8 | stringGsiPartitionKey, 9 | stringGsiSortKey, 10 | } from '@lib/decorators/helpers/gsi'; 11 | import { numberLsiSortKey, stringLsiSortKey } from '@lib/decorators/helpers/lsi'; 12 | import { array, binary, boolean, number, object, set, string } from '@lib/decorators/helpers/other'; 13 | import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix'; 14 | import { 15 | numberPartitionKey, 16 | numberSortKey, 17 | stringPartitionKey, 18 | stringSortKey, 19 | } from '@lib/decorators/helpers/primaryKey'; 20 | 21 | describe('Decorators', () => { 22 | describe('attribute', async () => { 23 | test('Should return proper basic attribute decorators', async () => { 24 | expect(attribute.string).toEqual(string); 25 | expect(attribute.number).toEqual(number); 26 | expect(attribute.boolean).toEqual(boolean); 27 | expect(attribute.object).toEqual(object); 28 | expect(attribute.array).toEqual(array); 29 | expect(attribute.set).toEqual(set); 30 | expect(attribute.binary).toEqual(binary); 31 | }); 32 | 33 | test('Should return proper date attribute decorators', async () => { 34 | expect(attribute.date.string).toEqual(stringDate); 35 | expect(attribute.date.number).toEqual(numberDate); 36 | }); 37 | 38 | test('Should return proper partitionKey attribute decorators', async () => { 39 | expect(attribute.partitionKey.string).toEqual(stringPartitionKey); 40 | expect(attribute.partitionKey.number).toEqual(numberPartitionKey); 41 | }); 42 | 43 | test('Should return proper sortKey attribute decorators', async () => { 44 | expect(attribute.sortKey.string).toEqual(stringSortKey); 45 | expect(attribute.sortKey.number).toEqual(numberSortKey); 46 | }); 47 | 48 | test('Should return proper gsi attribute decorators', async () => { 49 | expect(attribute.gsi.partitionKey.string).toEqual(stringGsiPartitionKey); 50 | expect(attribute.gsi.partitionKey.number).toEqual(numberGsiPartitionKey); 51 | expect(attribute.gsi.sortKey.string).toEqual(stringGsiSortKey); 52 | expect(attribute.gsi.sortKey.number).toEqual(numberGsiSortKey); 53 | }); 54 | 55 | test('Should return proper lsi attribute decorators', async () => { 56 | expect(attribute.lsi.sortKey.string).toEqual(stringLsiSortKey); 57 | expect(attribute.lsi.sortKey.number).toEqual(numberLsiSortKey); 58 | }); 59 | 60 | test('Should return proper prefix/suffix attribute decorators', async () => { 61 | expect(attribute.prefix).toEqual(prefix); 62 | expect(attribute.suffix).toEqual(suffix); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/unit/entity/helpers/returnValues.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import { 4 | ReturnValue as DynamoReturnValue, 5 | ReturnValuesOnConditionCheckFailure as DynamoReturnValueOnFailure, 6 | } from '@aws-sdk/client-dynamodb'; 7 | import { mapReturnValues, mapReturnValuesLimited } from '@lib/entity/helpers/returnValues'; 8 | 9 | describe('returnValues', () => { 10 | describe('mapReturnValues', async () => { 11 | test('Should properly map all DynamoReturnValues', async () => { 12 | expect(mapReturnValues('none')).toEqual(DynamoReturnValue.NONE); 13 | expect(mapReturnValues('allOld')).toEqual(DynamoReturnValue.ALL_OLD); 14 | expect(mapReturnValues('allNew')).toEqual(DynamoReturnValue.ALL_NEW); 15 | expect(mapReturnValues('updatedOld')).toEqual(DynamoReturnValue.UPDATED_OLD); 16 | expect(mapReturnValues('updatedNew')).toEqual(DynamoReturnValue.UPDATED_NEW); 17 | }); 18 | 19 | test('Should properly return default return value', async () => { 20 | expect(mapReturnValues()).toEqual(DynamoReturnValue.ALL_NEW); 21 | }); 22 | }); 23 | 24 | describe('mapReturnValuesLimited', async () => { 25 | test('Should properly map all DynamoReturnValueOnFailure', async () => { 26 | expect(mapReturnValuesLimited('none')).toEqual(DynamoReturnValueOnFailure.NONE); 27 | expect(mapReturnValuesLimited('allOld')).toEqual(DynamoReturnValueOnFailure.ALL_OLD); 28 | }); 29 | 30 | test('Should properly return default return value', async () => { 31 | expect(mapReturnValuesLimited()).toEqual(DynamoReturnValueOnFailure.ALL_OLD); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/builders.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 2 | 3 | import { GlobalSecondaryIndexUpdate, KeySchemaElement } from '@aws-sdk/client-dynamodb'; 4 | import { buildIndexCreate, buildIndexDelete } from '@lib/table/helpers/builders'; 5 | import * as schemaHelper from '@lib/table/helpers/schema'; 6 | import { BuildIndexCreate } from '@lib/table/types'; 7 | 8 | describe('buildIndexCreate', () => { 9 | let getKeySchemaSpy = vi.spyOn(schemaHelper, 'getKeySchema'); 10 | 11 | beforeEach(() => { 12 | getKeySchemaSpy = vi.spyOn(schemaHelper, 'getKeySchema'); 13 | }); 14 | 15 | afterEach(() => { 16 | vi.restoreAllMocks(); 17 | }); 18 | 19 | function createInput(throughput?: { read: number; write: number }): BuildIndexCreate { 20 | return { 21 | indexName: 'Index1', 22 | partitionKey: 'PK1', 23 | sortKey: 'SK1', 24 | throughput: throughput && { 25 | ReadCapacityUnits: throughput.read, 26 | WriteCapacityUnits: throughput.write, 27 | }, 28 | }; 29 | } 30 | 31 | it('Should return GlobalSecondaryIndexUpdate with the correct properties', () => { 32 | const input = createInput({ read: 10, write: 20 }); 33 | const keySchema = [ 34 | { AttributeName: 'PK1', KeyType: 'HASH' }, 35 | { AttributeName: 'SK1', KeyType: 'RANGE' }, 36 | ] as KeySchemaElement[]; 37 | getKeySchemaSpy.mockReturnValue(keySchema); 38 | 39 | const expectedOutput: GlobalSecondaryIndexUpdate[] = [ 40 | { 41 | Create: { 42 | IndexName: 'Index1', 43 | KeySchema: keySchema, 44 | Projection: { ProjectionType: 'ALL' }, 45 | ProvisionedThroughput: { 46 | ReadCapacityUnits: 10, 47 | WriteCapacityUnits: 20, 48 | }, 49 | }, 50 | }, 51 | ]; 52 | 53 | const result = buildIndexCreate(input); 54 | expect(result).toEqual(expectedOutput); 55 | expect(getKeySchemaSpy).toHaveBeenCalledTimes(1); 56 | }); 57 | 58 | it('Should return GlobalSecondaryIndexUpdate without ProvisionedThroughput when throughput is not provided', () => { 59 | const input = createInput(); 60 | getKeySchemaSpy.mockReturnValue([ 61 | { AttributeName: 'PK1', KeyType: 'HASH' }, 62 | { AttributeName: 'SK1', KeyType: 'RANGE' }, 63 | ]); 64 | 65 | const expectedOutput: GlobalSecondaryIndexUpdate[] = [ 66 | { 67 | Create: { 68 | IndexName: 'Index1', 69 | KeySchema: [ 70 | { AttributeName: 'PK1', KeyType: 'HASH' }, 71 | { AttributeName: 'SK1', KeyType: 'RANGE' }, 72 | ], 73 | Projection: { ProjectionType: 'ALL' }, 74 | }, 75 | }, 76 | ]; 77 | 78 | const result = buildIndexCreate(input); 79 | expect(result).toEqual(expectedOutput); 80 | expect(getKeySchemaSpy).toHaveBeenCalledTimes(1); 81 | }); 82 | }); 83 | 84 | describe('buildIndexDelete', () => { 85 | it('Should return GlobalSecondaryIndexUpdate array with Delete action for provided indexName', () => { 86 | const indexName = 'Index1'; 87 | const expectedOutput: GlobalSecondaryIndexUpdate[] = [ 88 | { 89 | Delete: { 90 | IndexName: indexName, 91 | }, 92 | }, 93 | ]; 94 | 95 | const result = buildIndexDelete(indexName); 96 | expect(result).toEqual(expectedOutput); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/definitions.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import { Dynamode } from '@lib/module'; 4 | import { getTableAttributeDefinitions } from '@lib/table/helpers/definitions'; 5 | import * as attributeHelper from '@lib/table/helpers/utils'; 6 | 7 | import { TestTable, TestTableManager, TestTableMetadata } from '../../../fixtures/TestTable'; 8 | 9 | describe('getTableAttributeDefinitions', () => { 10 | let getAttributeTypeSpy = vi.spyOn(attributeHelper, 'getAttributeType'); 11 | let getEntityAttributesSpy = vi.spyOn(Dynamode.storage, 'getEntityAttributes'); 12 | 13 | beforeEach(() => { 14 | getAttributeTypeSpy = vi.spyOn(attributeHelper, 'getAttributeType'); 15 | getEntityAttributesSpy = vi.spyOn(Dynamode.storage, 'getEntityAttributes'); 16 | }); 17 | 18 | afterEach(() => { 19 | vi.restoreAllMocks(); 20 | }); 21 | 22 | test('Should return AttributeDefinition array based on Partition Keys, Sort Keys and Indexes', async () => { 23 | getEntityAttributesSpy.mockReturnValue({ 24 | partitionKeyName: { 25 | propertyName: 'partitionKey', 26 | type: String, 27 | role: 'partitionKey', 28 | }, 29 | sortKeyName: { 30 | propertyName: 'sortKey', 31 | type: String, 32 | role: 'sortKey', 33 | }, 34 | indexPartitionKeyName: { 35 | propertyName: 'indexPartitionKey', 36 | type: String, 37 | role: 'index', 38 | indexes: [{ name: 'indexName', role: 'gsiPartitionKey' }], 39 | }, 40 | }); 41 | 42 | getAttributeTypeSpy 43 | .mockReturnValueOnce('S') 44 | .mockReturnValueOnce('N') 45 | .mockReturnValueOnce('S') 46 | .mockReturnValueOnce('N'); 47 | 48 | const metadata: TestTableMetadata = { 49 | ...TestTableManager.tableMetadata, 50 | indexes: { 51 | indexName: { 52 | partitionKey: 'indexPartitionKeyName', 53 | sortKey: 'indexSortKeyName', 54 | }, 55 | } as any, 56 | }; 57 | 58 | const expectedResult = [ 59 | { 60 | AttributeName: 'partitionKey', 61 | AttributeType: 'S', 62 | }, 63 | { 64 | AttributeName: 'sortKey', 65 | AttributeType: 'N', 66 | }, 67 | { 68 | AttributeName: 'indexPartitionKeyName', 69 | AttributeType: 'S', 70 | }, 71 | { 72 | AttributeName: 'indexSortKeyName', 73 | AttributeType: 'N', 74 | }, 75 | ]; 76 | 77 | const results = await getTableAttributeDefinitions(metadata as any, TestTable as any); 78 | 79 | expect(results).toEqual(expectedResult); 80 | expect(getEntityAttributesSpy).toHaveBeenCalledTimes(1); 81 | expect(getAttributeTypeSpy).toHaveBeenCalledTimes(4); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BillingMode, 3 | ScalarAttributeType, 4 | SSEStatus, 5 | SSEType, 6 | StreamViewType, 7 | TableClass, 8 | TableDescription, 9 | TableStatus, 10 | } from '@aws-sdk/client-dynamodb'; 11 | 12 | export const tableMetadata = { 13 | tableName: 'TableName', 14 | partitionKey: 'PK', 15 | indexes: { 16 | LSI: { 17 | sortKey: 'LSI_SK', 18 | }, 19 | GSI: { 20 | partitionKey: 'GSI_PK', 21 | }, 22 | }, 23 | }; 24 | 25 | export const validTableDescription = { 26 | TableName: 'TableName', 27 | TableStatus: TableStatus.ACTIVE, 28 | CreationDateTime: new Date(), 29 | AttributeDefinitions: [{ AttributeName: 'PK', AttributeType: ScalarAttributeType.S }], 30 | KeySchema: [{ AttributeName: 'PK', KeyType: 'HASH' }], 31 | ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, 32 | TableSizeBytes: 10, 33 | ItemCount: 1, 34 | TableArn: 'TableArn', 35 | TableId: 'TableId', 36 | BillingModeSummary: { BillingMode: BillingMode.PAY_PER_REQUEST }, 37 | LocalSecondaryIndexes: [ 38 | { 39 | IndexName: 'LSI', 40 | KeySchema: [{ AttributeName: 'LSI_SK', KeyType: 'RANGE' }], 41 | IndexSizeBytes: 1, 42 | ItemCount: 1, 43 | IndexArn: 'IndexArn', 44 | }, 45 | ], 46 | GlobalSecondaryIndexes: [ 47 | { 48 | IndexName: 'GSI', 49 | KeySchema: [{ AttributeName: 'GSI_PK', KeyType: 'HASH' }], 50 | IndexSizeBytes: 1, 51 | ItemCount: 1, 52 | IndexArn: 'IndexArn', 53 | ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, 54 | }, 55 | ], 56 | StreamSpecification: { StreamEnabled: true, StreamViewType: StreamViewType.NEW_AND_OLD_IMAGES }, 57 | LatestStreamLabel: 'LatestStreamLabel', 58 | LatestStreamArn: 'LatestStreamArn', 59 | GlobalTableVersion: '2020.12.11', 60 | Replicas: [{ RegionName: 'RegionName' }], 61 | RestoreSummary: { 62 | SourceBackupArn: 'SourceBackupArn', 63 | SourceTableArn: 'SourceTableArn', 64 | RestoreDateTime: new Date(), 65 | RestoreInProgress: false, 66 | }, 67 | SSEDescription: { Status: SSEStatus.ENABLED, SSEType: SSEType.AES256, KMSMasterKeyArn: 'KMSMasterKeyArn' }, 68 | ArchivalSummary: { 69 | ArchivalDateTime: new Date(), 70 | ArchivalReason: 'ArchivalReason', 71 | ArchivalBackupArn: 'ArchivalBackupArn', 72 | }, 73 | TableClassSummary: { TableClass: TableClass.STANDARD }, 74 | DeletionProtectionEnabled: false, 75 | } satisfies TableDescription; 76 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/schema.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import { KeySchemaElement } from '@aws-sdk/client-dynamodb'; 4 | import { getKeySchema } from '@lib/table/helpers/schema'; 5 | 6 | describe('getKeySchema', () => { 7 | // Test when only the partition key is provided 8 | test('Should return HASH key schema when no sort key is provided', () => { 9 | const partitionKey = 'PartitionKey'; 10 | const expectedSchema: KeySchemaElement[] = [ 11 | { 12 | AttributeName: partitionKey, 13 | KeyType: 'HASH', 14 | }, 15 | ]; 16 | 17 | const schema = getKeySchema(partitionKey); 18 | 19 | expect(schema).toEqual(expectedSchema); 20 | }); 21 | 22 | // Test when both the partition and sort keys are provided 23 | test('Should return HASH and RANGE key schema when sort key is provided', () => { 24 | const partitionKey = 'PartitionKey'; 25 | const sortKey = 'SortKey'; 26 | const expectedSchema: KeySchemaElement[] = [ 27 | { 28 | AttributeName: partitionKey, 29 | KeyType: 'HASH', 30 | }, 31 | { 32 | AttributeName: sortKey, 33 | KeyType: 'RANGE', 34 | }, 35 | ]; 36 | 37 | const schema = getKeySchema(partitionKey, sortKey); 38 | 39 | expect(schema).toEqual(expectedSchema); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import { AttributesMetadata } from '@lib/dynamode/storage/types'; 4 | import { compareDynamodeEntityWithDynamoTable, getAttributeType } from '@lib/table/helpers/utils'; 5 | import { ConflictError, ValidationError } from '@lib/utils'; 6 | 7 | describe('compareDynamodeEntityWithDynamoTable', () => { 8 | const dynamodeSchema = [ 9 | { AttributeName: 'id', KeyType: 'HASH' }, 10 | { AttributeName: 'timestamp', KeyType: 'RANGE' }, 11 | ]; 12 | 13 | test('Should not throw an error if the schemas are identical', () => { 14 | const identicalSchema = [...dynamodeSchema]; 15 | expect(() => compareDynamodeEntityWithDynamoTable(dynamodeSchema, identicalSchema)).not.toThrow(); 16 | expect(() => compareDynamodeEntityWithDynamoTable(dynamodeSchema, identicalSchema.reverse())).not.toThrow(); 17 | }); 18 | 19 | test('Should throw a ConflictError if an attribute is missing in the dynamoDB schema', () => { 20 | const missingAttributeSchema = [ 21 | { AttributeName: 'id', KeyType: 'HASH' }, 22 | { AttributeName: 'test', KeyType: 'HASH' }, 23 | ]; 24 | expect(() => compareDynamodeEntityWithDynamoTable(dynamodeSchema, missingAttributeSchema)).toThrow(ConflictError); 25 | expect(() => compareDynamodeEntityWithDynamoTable(dynamodeSchema, missingAttributeSchema)).toThrowError( 26 | 'Key "{"AttributeName":"timestamp","KeyType":"RANGE"}" not found in table', 27 | ); 28 | }); 29 | 30 | test('Should throw a ConflictError if an attribute is missing in the dynamode schema', () => { 31 | expect(() => 32 | compareDynamodeEntityWithDynamoTable( 33 | [...dynamodeSchema, { AttributeName: 'id', KeyType: 'HASH' }], 34 | [...dynamodeSchema, { AttributeName: 'name', KeyType: 'RANGE' }], 35 | ), 36 | ).toThrow(ConflictError); 37 | expect(() => 38 | compareDynamodeEntityWithDynamoTable( 39 | [...dynamodeSchema, { AttributeName: 'id', KeyType: 'HASH' }], 40 | [...dynamodeSchema, { AttributeName: 'name', KeyType: 'RANGE' }], 41 | ), 42 | ).toThrowError('Key "{"AttributeName":"name","KeyType":"RANGE"}" not found in entity'); 43 | }); 44 | }); 45 | 46 | describe('getAttributeType', () => { 47 | const mockAttributesMetadata: AttributesMetadata = { 48 | name: { 49 | propertyName: 'Name', 50 | type: String, 51 | role: 'attribute', 52 | }, 53 | age: { 54 | propertyName: 'Age', 55 | type: Number, 56 | role: 'attribute', 57 | }, 58 | invalidProp: { 59 | propertyName: 'InvalidProp', 60 | type: Boolean, 61 | role: 'attribute', 62 | }, 63 | }; 64 | 65 | test('Should return "S" for String type attribute', () => { 66 | const attributeType = getAttributeType(mockAttributesMetadata, 'name'); 67 | expect(attributeType).toEqual('S'); 68 | }); 69 | 70 | test('Should return "N" for Number type attribute', () => { 71 | const attributeType = getAttributeType(mockAttributesMetadata, 'age'); 72 | expect(attributeType).toEqual('N'); 73 | }); 74 | 75 | test('Should throw ValidationError for invalid attribute type', () => { 76 | expect(() => getAttributeType(mockAttributesMetadata, 'invalidProp')).toThrow(ValidationError); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/unit/table/helpers/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 2 | 3 | import * as definitionsHelper from '@lib/table/helpers/definitions'; 4 | import * as indexesHelpers from '@lib/table/helpers/indexes'; 5 | import * as schemaHelper from '@lib/table/helpers/schema'; 6 | import * as utilsHelper from '@lib/table/helpers/utils'; 7 | import { validateTable } from '@lib/table/helpers/validator'; 8 | 9 | import { tableMetadata, validTableDescription } from './fixtures'; 10 | 11 | describe('validateTable', () => { 12 | let getKeySchemaSpy = vi.spyOn(schemaHelper, 'getKeySchema'); 13 | let getTableAttributeDefinitionsSpy = vi.spyOn(definitionsHelper, 'getTableAttributeDefinitions'); 14 | let getTableLocalSecondaryIndexesSpy = vi.spyOn(indexesHelpers, 'getTableLocalSecondaryIndexes'); 15 | let getTableGlobalSecondaryIndexesSpy = vi.spyOn(indexesHelpers, 'getTableGlobalSecondaryIndexes'); 16 | let compareDynamodeEntityWithDynamoTableSpy = vi.spyOn(utilsHelper, 'compareDynamodeEntityWithDynamoTable'); 17 | 18 | beforeEach(() => { 19 | getKeySchemaSpy = vi.spyOn(schemaHelper, 'getKeySchema'); 20 | getTableAttributeDefinitionsSpy = vi.spyOn(definitionsHelper, 'getTableAttributeDefinitions'); 21 | getTableLocalSecondaryIndexesSpy = vi.spyOn(indexesHelpers, 'getTableLocalSecondaryIndexes'); 22 | getTableGlobalSecondaryIndexesSpy = vi.spyOn(indexesHelpers, 'getTableGlobalSecondaryIndexes'); 23 | compareDynamodeEntityWithDynamoTableSpy = vi.spyOn(utilsHelper, 'compareDynamodeEntityWithDynamoTable'); 24 | }); 25 | 26 | afterEach(() => { 27 | vi.restoreAllMocks(); 28 | }); 29 | 30 | test('Should validate the table correctly', () => { 31 | getKeySchemaSpy.mockReturnValueOnce(validTableDescription.KeySchema); 32 | getTableAttributeDefinitionsSpy.mockReturnValueOnce(validTableDescription.AttributeDefinitions); 33 | getTableLocalSecondaryIndexesSpy.mockReturnValueOnce([ 34 | { 35 | IndexName: 'LSI', 36 | KeySchema: [{ AttributeName: 'LSI_SK', KeyType: 'RANGE' }], 37 | Projection: { ProjectionType: 'ALL' }, 38 | }, 39 | ]); 40 | getTableGlobalSecondaryIndexesSpy.mockReturnValueOnce([ 41 | { 42 | IndexName: 'GSI', 43 | KeySchema: [{ AttributeName: 'GSI_PK', KeyType: 'HASH' }], 44 | Projection: { ProjectionType: 'ALL' }, 45 | }, 46 | ]); 47 | 48 | validateTable({ 49 | metadata: tableMetadata as any, 50 | tableNameEntity: 'tableNameEntity', 51 | table: validTableDescription, 52 | }); 53 | 54 | expect(getKeySchemaSpy).toHaveBeenCalledTimes(1); 55 | expect(getTableAttributeDefinitionsSpy).toHaveBeenCalledTimes(1); 56 | expect(getTableLocalSecondaryIndexesSpy).toHaveBeenCalledTimes(1); 57 | expect(getTableGlobalSecondaryIndexesSpy).toHaveBeenCalledTimes(1); 58 | expect(compareDynamodeEntityWithDynamoTableSpy).toHaveBeenCalledTimes(4); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/unit/utils/converter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from 'vitest'; 2 | 3 | import ddbConverter from '@lib/dynamode/converter'; 4 | import { fromDynamo, objectToDynamo, valueFromDynamo, valueToDynamo } from '@lib/utils'; 5 | 6 | const marshallSpy = vi.fn(); 7 | const convertToAttrSpy = vi.fn(); 8 | const convertToNativeSpy = vi.fn(); 9 | const unmarshallSpy = vi.fn(); 10 | 11 | vi.mock('@lib/dynamode/converter', () => ({ 12 | default: { 13 | get: vi.fn().mockImplementation(() => ({ 14 | marshall: marshallSpy, 15 | convertToAttr: convertToAttrSpy, 16 | convertToNative: convertToNativeSpy, 17 | unmarshall: unmarshallSpy, 18 | })), 19 | }, 20 | })); 21 | 22 | describe('Helpers', () => { 23 | const converterGetSpy = vi.spyOn(ddbConverter, 'get'); 24 | 25 | describe('objectToDynamo', () => { 26 | test("Should run converter's marshall function", async () => { 27 | objectToDynamo({ a: 1, b: 2 }); 28 | expect(converterGetSpy).toHaveBeenCalled(); 29 | expect(marshallSpy).toHaveBeenCalled(); 30 | }); 31 | }); 32 | 33 | describe('valueToDynamo', () => { 34 | test("Should run converter's marshall function", async () => { 35 | valueToDynamo('value'); 36 | expect(converterGetSpy).toHaveBeenCalled(); 37 | expect(convertToAttrSpy).toHaveBeenCalled(); 38 | }); 39 | }); 40 | 41 | describe('valueFromDynamo', () => { 42 | test("Should run converter's convertToNative function", async () => { 43 | valueFromDynamo({ S: '1' }); 44 | expect(converterGetSpy).toHaveBeenCalled(); 45 | expect(convertToAttrSpy).toHaveBeenCalled(); 46 | }); 47 | }); 48 | 49 | describe('fromDynamo', () => { 50 | test("Should run converter's unmarshall function", async () => { 51 | fromDynamo({ prop: { S: '1' } }); 52 | expect(converterGetSpy).toHaveBeenCalled(); 53 | expect(convertToAttrSpy).toHaveBeenCalled(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/unit/utils/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | import { createError } from '@lib/utils'; 4 | 5 | describe('Helpers', () => { 6 | describe('createError', () => { 7 | const message = 'message'; 8 | const name = 'name'; 9 | const CustomError = createError(message, name); 10 | 11 | test('Should create error instance', async () => { 12 | const errorInstance = new CustomError(); 13 | expect(errorInstance.message).toEqual(message); 14 | expect(errorInstance.name).toEqual(name); 15 | }); 16 | 17 | test('Should create error instance with custom message', async () => { 18 | const customMessage = 'custom message'; 19 | const errorInstance = new CustomError(customMessage); 20 | expect(errorInstance.message).toEqual(customMessage); 21 | expect(errorInstance.name).toEqual(name); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "lib": ["ES2015"], 7 | "moduleResolution": "node", 8 | "rootDir": "lib", 9 | "baseUrl": "lib", 10 | "outDir": "dist", 11 | "paths": { 12 | "@lib/*": ["./*"], 13 | }, 14 | "strict": true, 15 | "allowSyntheticDefaultImports": true, 16 | "declaration": true, 17 | "esModuleInterop": true, 18 | "experimentalDecorators": true 19 | }, 20 | "include": ["lib"], 21 | "exclude": [ 22 | "node_modules", 23 | "dist", 24 | "docs", 25 | "examples", 26 | "tests", 27 | ], 28 | } -------------------------------------------------------------------------------- /tsconfig.typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@lib/*": ["./lib/*"], 8 | }, 9 | "noEmit": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["lib", "examples", "tests"], 13 | "exclude": ["node_modules", "dist", "docs"] 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | build: { 7 | commonjsOptions: { include: [] }, 8 | }, 9 | test: { 10 | include: ['tests/**/*.test.ts'], 11 | exclude: ['**.json'], 12 | coverage: { 13 | provider: 'v8', 14 | reporter: ['text', 'html', 'lcovonly'], 15 | include: ['lib/**'], 16 | exclude: ['lib/module.ts', 'lib/index.ts', '**/types.ts'], 17 | all: true, 18 | thresholds: { 19 | lines: 80, 20 | functions: 80, 21 | branches: 80, 22 | statements: 80, 23 | }, 24 | }, 25 | typecheck: { 26 | checker: 'tsc', 27 | include: ['tests/types/**/*.ts'], 28 | exclude: ['node_modules/**'], 29 | tsconfig: 'tests/tsconfig.json', 30 | ignoreSourceErrors: true, 31 | }, 32 | hookTimeout: 30000, 33 | }, 34 | }); 35 | --------------------------------------------------------------------------------