├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── img ├── get-item-attributes.gif ├── get-item-projection.gif ├── get-item-response.gif ├── get-item.gif ├── get-order.png ├── put-item-expression.gif └── query-expression.gif ├── package-lock.json ├── package.json ├── src ├── attribute-value.ts ├── callback.ts ├── client-v2.ts ├── client-v3.ts ├── create-set.ts ├── delete-document-command.ts ├── delete-item-command.ts ├── delete-item.ts ├── document-client-field-mappings.ts ├── document-client-v2.ts ├── document-client-v3.ts ├── expression-attributes.ts ├── expression.ts ├── get-command.ts ├── get-document-command.ts ├── get-item-command.ts ├── get-item.ts ├── index.ts ├── json-format.ts ├── key.ts ├── letter.ts ├── marshall.ts ├── narrow.ts ├── projection.ts ├── put-document-command.ts ├── put-item-command.ts ├── put-item.ts ├── query-command.ts ├── query-document-command.ts ├── query.ts ├── scan-command.ts ├── scan-document-command.ts ├── scan.ts ├── simplify.ts ├── stream-event.ts ├── update-document-command.ts ├── update-item-command.ts └── update-item.ts ├── test ├── expression-attributes.test.ts ├── marshall.test.ts ├── stream-event.test.ts ├── tsconfig.json ├── v2-document.test.ts ├── v2.test.ts ├── v3-document.test.ts └── v3.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "**", 38 | "!.projenrc.js" 39 | ], 40 | "rules": { 41 | "indent": [ 42 | "off" 43 | ], 44 | "@typescript-eslint/indent": [ 45 | "error", 46 | 2 47 | ], 48 | "quotes": [ 49 | "error", 50 | "single", 51 | { 52 | "avoidEscape": true 53 | } 54 | ], 55 | "comma-dangle": [ 56 | "error", 57 | "always-multiline" 58 | ], 59 | "comma-spacing": [ 60 | "error", 61 | { 62 | "before": false, 63 | "after": true 64 | } 65 | ], 66 | "no-multi-spaces": [ 67 | "error", 68 | { 69 | "ignoreEOLComments": false 70 | } 71 | ], 72 | "array-bracket-spacing": [ 73 | "error", 74 | "never" 75 | ], 76 | "array-bracket-newline": [ 77 | "error", 78 | "consistent" 79 | ], 80 | "object-curly-spacing": [ 81 | "error", 82 | "always" 83 | ], 84 | "object-curly-newline": [ 85 | "error", 86 | { 87 | "multiline": true, 88 | "consistent": true 89 | } 90 | ], 91 | "object-property-newline": [ 92 | "error", 93 | { 94 | "allowAllPropertiesOnSameLine": true 95 | } 96 | ], 97 | "keyword-spacing": [ 98 | "error" 99 | ], 100 | "brace-style": [ 101 | "error", 102 | "1tbs", 103 | { 104 | "allowSingleLine": true 105 | } 106 | ], 107 | "space-before-blocks": [ 108 | "error" 109 | ], 110 | "curly": [ 111 | "error", 112 | "multi-line", 113 | "consistent" 114 | ], 115 | "@typescript-eslint/member-delimiter-style": [ 116 | "error" 117 | ], 118 | "semi": [ 119 | "error", 120 | "always" 121 | ], 122 | "max-len": [ 123 | "error", 124 | { 125 | "code": 150, 126 | "ignoreUrls": true, 127 | "ignoreStrings": true, 128 | "ignoreTemplateLiterals": true, 129 | "ignoreComments": true, 130 | "ignoreRegExpLiterals": true 131 | } 132 | ], 133 | "quote-props": [ 134 | "error", 135 | "consistent-as-needed" 136 | ], 137 | "@typescript-eslint/no-require-imports": [ 138 | "error" 139 | ], 140 | "import/no-extraneous-dependencies": [ 141 | "error", 142 | { 143 | "devDependencies": [ 144 | "**/test/**", 145 | "**/build-tools/**" 146 | ], 147 | "optionalDependencies": false, 148 | "peerDependencies": true 149 | } 150 | ], 151 | "import/no-unresolved": [ 152 | "error" 153 | ], 154 | "import/order": [ 155 | "warn", 156 | { 157 | "groups": [ 158 | "builtin", 159 | "external" 160 | ], 161 | "alphabetize": { 162 | "order": "asc", 163 | "caseInsensitive": true 164 | } 165 | } 166 | ], 167 | "import/no-duplicates": [ 168 | "error" 169 | ], 170 | "no-shadow": [ 171 | "off" 172 | ], 173 | "@typescript-eslint/no-shadow": [ 174 | "error" 175 | ], 176 | "key-spacing": [ 177 | "error" 178 | ], 179 | "no-multiple-empty-lines": [ 180 | "error" 181 | ], 182 | "@typescript-eslint/no-floating-promises": [ 183 | "error" 184 | ], 185 | "no-return-await": [ 186 | "off" 187 | ], 188 | "@typescript-eslint/return-await": [ 189 | "error" 190 | ], 191 | "no-trailing-spaces": [ 192 | "error" 193 | ], 194 | "dot-notation": [ 195 | "error" 196 | ], 197 | "no-bitwise": [ 198 | "error" 199 | ], 200 | "@typescript-eslint/member-ordering": [ 201 | "error", 202 | { 203 | "default": [ 204 | "public-static-field", 205 | "public-static-method", 206 | "protected-static-field", 207 | "protected-static-method", 208 | "private-static-field", 209 | "private-static-method", 210 | "field", 211 | "constructor", 212 | "method" 213 | ] 214 | } 215 | ] 216 | }, 217 | "overrides": [ 218 | { 219 | "files": [ 220 | ".projenrc.js" 221 | ], 222 | "rules": { 223 | "@typescript-eslint/no-require-imports": "off", 224 | "import/no-extraneous-dependencies": "off" 225 | } 226 | } 227 | ] 228 | } 229 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/pull-request-lint.yml linguist-generated 10 | /.github/workflows/release.yml linguist-generated 11 | /.github/workflows/upgrade-main.yml linguist-generated 12 | /.gitignore linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/files.json linguist-generated 18 | /.projen/tasks.json linguist-generated 19 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /tsconfig.json linguist-generated 23 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Install dependencies 23 | run: yarn install --check-files 24 | - name: build 25 | run: npx projen build 26 | - name: Find mutations 27 | id: self_mutation 28 | run: |- 29 | git add . 30 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 31 | working-directory: ./ 32 | - name: Upload patch 33 | if: steps.self_mutation.outputs.self_mutation_happened 34 | uses: actions/upload-artifact@v4.4.0 35 | with: 36 | name: repo.patch 37 | path: repo.patch 38 | overwrite: true 39 | - name: Fail build on mutation 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | run: |- 42 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 43 | cat repo.patch 44 | exit 1 45 | self-mutation: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 56 | ref: ${{ github.event.pull_request.head.ref }} 57 | repository: ${{ github.event.pull_request.head.repo.full_name }} 58 | - name: Download patch 59 | uses: actions/download-artifact@v4 60 | with: 61 | name: repo.patch 62 | path: ${{ runner.temp }} 63 | - name: Apply patch 64 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 65 | - name: Set git identity 66 | run: |- 67 | git config user.name "github-actions" 68 | git config user.email "github-actions@github.com" 69 | - name: Push changes 70 | env: 71 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 72 | run: |- 73 | git add . 74 | git commit -s -m "chore: self mutation" 75 | git push origin HEAD:$PULL_REQUEST_REF 76 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.4.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Check if version has already been tagged 36 | id: check_tag_exists 37 | run: |- 38 | TAG=$(cat dist/releasetag.txt) 39 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 40 | cat $GITHUB_OUTPUT 41 | - name: Check for new commits 42 | id: git_remote 43 | run: |- 44 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 45 | cat $GITHUB_OUTPUT 46 | - name: Backup artifact permissions 47 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 48 | run: cd dist && getfacl -R . > permissions-backup.acl 49 | continue-on-error: true 50 | - name: Upload artifact 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | uses: actions/upload-artifact@v4.4.0 53 | with: 54 | name: build-artifact 55 | path: dist 56 | overwrite: true 57 | release_github: 58 | name: Publish to GitHub Releases 59 | needs: 60 | - release 61 | - release_npm 62 | runs-on: ubuntu-latest 63 | permissions: 64 | contents: write 65 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 66 | steps: 67 | - uses: actions/setup-node@v4 68 | with: 69 | node-version: lts/* 70 | - name: Download build artifacts 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: build-artifact 74 | path: dist 75 | - name: Restore build artifact permissions 76 | run: cd dist && setfacl --restore=permissions-backup.acl 77 | continue-on-error: true 78 | - name: Release 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | GITHUB_REPOSITORY: ${{ github.repository }} 82 | GITHUB_REF: ${{ github.sha }} 83 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 84 | release_npm: 85 | name: Publish to npm 86 | needs: release 87 | runs-on: ubuntu-latest 88 | permissions: 89 | id-token: write 90 | contents: read 91 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 92 | steps: 93 | - uses: actions/setup-node@v4 94 | with: 95 | node-version: lts/* 96 | - name: Download build artifacts 97 | uses: actions/download-artifact@v4 98 | with: 99 | name: build-artifact 100 | path: dist 101 | - name: Restore build artifact permissions 102 | run: cd dist && setfacl --restore=permissions-backup.acl 103 | continue-on-error: true 104 | - name: Release 105 | env: 106 | NPM_DIST_TAG: latest 107 | NPM_REGISTRY: registry.npmjs.org 108 | NPM_CONFIG_PROVENANCE: "true" 109 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 110 | run: npx -p publib@latest publib-npm 111 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: main 50 | - name: Download patch 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: repo.patch 54 | path: ${{ runner.temp }} 55 | - name: Apply patch 56 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 57 | - name: Set git identity 58 | run: |- 59 | git config user.name "github-actions" 60 | git config user.email "github-actions@github.com" 61 | - name: Create Pull Request 62 | id: create-pr 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | commit-message: |- 67 | chore(deps): upgrade dependencies 68 | 69 | Upgrades project dependencies. See details in [workflow run]. 70 | 71 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 72 | 73 | ------ 74 | 75 | *Automatically created by projen via the "upgrade-main" workflow* 76 | branch: github-actions/upgrade-main 77 | title: "chore(deps): upgrade dependencies" 78 | body: |- 79 | Upgrades project dependencies. See details in [workflow run]. 80 | 81 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 82 | 83 | ------ 84 | 85 | *Automatically created by projen via the "upgrade-main" workflow* 86 | author: github-actions 87 | committer: github-actions 88 | signoff: true 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | .DS_Store 34 | .dccache 35 | /test-reports/ 36 | junit.xml 37 | /coverage/ 38 | !/.github/workflows/build.yml 39 | /dist/changelog.md 40 | /dist/version.txt 41 | !/.github/workflows/release.yml 42 | !/.mergify.yml 43 | !/.github/workflows/upgrade-main.yml 44 | !/.github/pull_request_template.md 45 | !/test/ 46 | !/tsconfig.json 47 | !/tsconfig.dev.json 48 | !/src/ 49 | /lib 50 | /dist/ 51 | !/.eslintrc.json 52 | !/.projenrc.js 53 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | pull_request_rules: 11 | - name: Automatic merge on approval and successful build 12 | actions: 13 | delete_head_branch: {} 14 | queue: 15 | method: squash 16 | name: default 17 | commit_message_template: |- 18 | {{ title }} (#{{ number }}) 19 | 20 | {{ body }} 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | /.gitattributes 25 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/node", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@typescript-eslint/eslint-plugin", 13 | "version": "^7", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@typescript-eslint/parser", 18 | "version": "^7", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "commit-and-tag-version", 23 | "version": "^12", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "constructs", 28 | "version": "^10.0.0", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-import-resolver-typescript", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint-plugin-import", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint", 41 | "version": "^8", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "jest", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest-junit", 50 | "version": "^15", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "projen", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "ts-jest", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "typescript", 63 | "version": "latest", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "@aws-sdk/client-dynamodb", 68 | "type": "peer" 69 | }, 70 | { 71 | "name": "@aws-sdk/lib-dynamodb", 72 | "type": "peer" 73 | }, 74 | { 75 | "name": "@aws-sdk/types", 76 | "type": "peer" 77 | }, 78 | { 79 | "name": "@aws-sdk/util-dynamodb", 80 | "type": "peer" 81 | }, 82 | { 83 | "name": "@smithy/smithy-client", 84 | "type": "peer" 85 | }, 86 | { 87 | "name": "@types/aws-lambda", 88 | "type": "peer" 89 | }, 90 | { 91 | "name": "aws-sdk", 92 | "type": "peer" 93 | } 94 | ], 95 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 96 | } 97 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/release.yml", 9 | ".github/workflows/upgrade-main.yml", 10 | ".gitignore", 11 | ".mergify.yml", 12 | ".npmignore", 13 | ".projen/deps.json", 14 | ".projen/files.json", 15 | ".projen/tasks.json", 16 | "LICENSE", 17 | "tsconfig.dev.json", 18 | "tsconfig.json" 19 | ], 20 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 21 | } 22 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 37 | }, 38 | "steps": [ 39 | { 40 | "builtin": "release/bump-version" 41 | } 42 | ], 43 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 44 | }, 45 | "clobber": { 46 | "name": "clobber", 47 | "description": "hard resets to HEAD of origin and cleans the local repo", 48 | "env": { 49 | "BRANCH": "$(git branch --show-current)" 50 | }, 51 | "steps": [ 52 | { 53 | "exec": "git checkout -b scratch", 54 | "name": "save current HEAD in \"scratch\" branch" 55 | }, 56 | { 57 | "exec": "git checkout $BRANCH" 58 | }, 59 | { 60 | "exec": "git fetch origin", 61 | "name": "fetch latest changes from origin" 62 | }, 63 | { 64 | "exec": "git reset --hard origin/$BRANCH", 65 | "name": "hard reset to origin commit" 66 | }, 67 | { 68 | "exec": "git clean -fdx", 69 | "name": "clean all untracked files" 70 | }, 71 | { 72 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 73 | } 74 | ], 75 | "condition": "git diff --exit-code > /dev/null" 76 | }, 77 | "compile": { 78 | "name": "compile", 79 | "description": "Only compile", 80 | "steps": [ 81 | { 82 | "exec": "tsc --build" 83 | } 84 | ] 85 | }, 86 | "default": { 87 | "name": "default", 88 | "description": "Synthesize project files", 89 | "steps": [ 90 | { 91 | "exec": "node .projenrc.js" 92 | } 93 | ] 94 | }, 95 | "eject": { 96 | "name": "eject", 97 | "description": "Remove projen from the project", 98 | "env": { 99 | "PROJEN_EJECTING": "true" 100 | }, 101 | "steps": [ 102 | { 103 | "spawn": "default" 104 | } 105 | ] 106 | }, 107 | "eslint": { 108 | "name": "eslint", 109 | "description": "Runs eslint against the codebase", 110 | "steps": [ 111 | { 112 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools .projenrc.js", 113 | "receiveArgs": true 114 | } 115 | ] 116 | }, 117 | "install": { 118 | "name": "install", 119 | "description": "Install project dependencies and update lockfile (non-frozen)", 120 | "steps": [ 121 | { 122 | "exec": "yarn install --check-files" 123 | } 124 | ] 125 | }, 126 | "install:ci": { 127 | "name": "install:ci", 128 | "description": "Install project dependencies using frozen lockfile", 129 | "steps": [ 130 | { 131 | "exec": "yarn install --check-files --frozen-lockfile" 132 | } 133 | ] 134 | }, 135 | "package": { 136 | "name": "package", 137 | "description": "Creates the distribution package", 138 | "steps": [ 139 | { 140 | "exec": "mkdir -p dist/js" 141 | }, 142 | { 143 | "exec": "npm pack --pack-destination dist/js" 144 | } 145 | ] 146 | }, 147 | "post-compile": { 148 | "name": "post-compile", 149 | "description": "Runs after successful compilation" 150 | }, 151 | "post-upgrade": { 152 | "name": "post-upgrade", 153 | "description": "Runs after upgrading dependencies" 154 | }, 155 | "pre-compile": { 156 | "name": "pre-compile", 157 | "description": "Prepare the project for compilation" 158 | }, 159 | "release": { 160 | "name": "release", 161 | "description": "Prepare a release from \"main\" branch", 162 | "env": { 163 | "RELEASE": "true" 164 | }, 165 | "steps": [ 166 | { 167 | "exec": "rm -fr dist" 168 | }, 169 | { 170 | "spawn": "bump" 171 | }, 172 | { 173 | "spawn": "build" 174 | }, 175 | { 176 | "spawn": "unbump" 177 | }, 178 | { 179 | "exec": "git diff --ignore-space-at-eol --exit-code" 180 | } 181 | ] 182 | }, 183 | "test": { 184 | "name": "test", 185 | "description": "Run tests", 186 | "steps": [ 187 | { 188 | "exec": "jest --passWithNoTests --updateSnapshot", 189 | "receiveArgs": true 190 | }, 191 | { 192 | "spawn": "eslint" 193 | } 194 | ] 195 | }, 196 | "test:watch": { 197 | "name": "test:watch", 198 | "description": "Run jest in watch mode", 199 | "steps": [ 200 | { 201 | "exec": "jest --watch" 202 | } 203 | ] 204 | }, 205 | "unbump": { 206 | "name": "unbump", 207 | "description": "Restores version to 0.0.0", 208 | "env": { 209 | "OUTFILE": "package.json", 210 | "CHANGELOG": "dist/changelog.md", 211 | "BUMPFILE": "dist/version.txt", 212 | "RELEASETAG": "dist/releasetag.txt", 213 | "RELEASE_TAG_PREFIX": "", 214 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 215 | }, 216 | "steps": [ 217 | { 218 | "builtin": "release/reset-version" 219 | } 220 | ] 221 | }, 222 | "upgrade": { 223 | "name": "upgrade", 224 | "description": "upgrade dependencies", 225 | "env": { 226 | "CI": "0" 227 | }, 228 | "steps": [ 229 | { 230 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,projen,ts-jest,@aws-sdk/client-dynamodb,@aws-sdk/lib-dynamodb,@aws-sdk/types,@aws-sdk/util-dynamodb,@smithy/smithy-client,@types/aws-lambda,aws-sdk" 231 | }, 232 | { 233 | "exec": "yarn install --check-files" 234 | }, 235 | { 236 | "exec": "yarn upgrade @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit projen ts-jest typescript @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb @aws-sdk/types @aws-sdk/util-dynamodb @smithy/smithy-client @types/aws-lambda aws-sdk" 237 | }, 238 | { 239 | "exec": "npx projen" 240 | }, 241 | { 242 | "spawn": "post-upgrade" 243 | } 244 | ] 245 | }, 246 | "watch": { 247 | "name": "watch", 248 | "description": "Watch & compile in the background", 249 | "steps": [ 250 | { 251 | "exec": "tsc --build -w" 252 | } 253 | ] 254 | } 255 | }, 256 | "env": { 257 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 258 | }, 259 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 260 | } 261 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { typescript } = require('projen'); 2 | const project = new typescript.TypeScriptProject({ 3 | defaultReleaseBranch: 'main', 4 | name: 'typesafe-dynamodb', 5 | typescriptVersion: 'latest', 6 | repository: 'https://github.com/sam-goodwin/typesafe-dynamodb', 7 | peerDeps: [ 8 | 'aws-sdk', 9 | '@aws-sdk/client-dynamodb', 10 | '@aws-sdk/lib-dynamodb', 11 | '@aws-sdk/types', 12 | '@aws-sdk/util-dynamodb', 13 | '@smithy/smithy-client', 14 | '@types/aws-lambda', 15 | ], 16 | eslintOptions: { 17 | ignorePatterns: ['**'], 18 | }, 19 | tsconfig: { 20 | compilerOptions: { 21 | lib: ['dom'], 22 | }, 23 | }, 24 | gitignore: ['.DS_Store', '.dccache'], 25 | releaseToNpm: true, 26 | }); 27 | 28 | project.synth(); 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[json]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[jsonc]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | }, 18 | "[markdown]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true, 21 | "editor.quickSuggestions": { 22 | "comments": "on", 23 | "strings": "on", 24 | "other": "on" 25 | }, 26 | "editor.suggest.showReferences": true, 27 | "editor.wordWrap": "on" 28 | }, 29 | "[yaml]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "editor.formatOnSave": true 32 | }, 33 | "editor.tabSize": 2, 34 | "editor.insertSpaces": true, 35 | "editor.formatOnSave": true, 36 | "eslint.format.enable": false 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typesafe-dynamodb 2 | 3 | [![npm version](https://badge.fury.io/js/typesafe-dynamodb.svg)](https://badge.fury.io/js/typesafe-dynamodb) 4 | 5 | `typesafe-dynamodb` is a type-only library which replaces the type signatures of the AWS SDK's DynamoDB client. It substitutes `getItem`, `putItem`, `deleteItem` and `query` API methods with type-safe alternatives that are aware of the data in your tables and also adaptive to the semantics of the API request, e.g. by validating `ExpressionAttributeNames` and `ExpressionAttributeValues` contain all the values used in a `ConditionExpression` string, or by understanding the effect of a `ProjectionExpression` on the returned data type. 6 | 7 | The end goal is to provide types that have total understanding of the AWS DynamoDB API and enable full utilization of the TypeScript type system for modeling complex DynamoDB tables, such as the application of union types and template string literals for single-table designs. 8 | 9 | ![typesafe putItem ConditionExpression](img/put-item-expression.gif) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install --save-dev typesafe-dynamodb 15 | ``` 16 | 17 | ## Usage 18 | 19 | This library contains type definitions for both AWS SDK v2 (`aws-sdk`) and AWS SDK v3 (`@aws-sdk/client-dynamodb`); 20 | 21 | ### AWS SDK v2 22 | 23 | To use `typesafe-dynamodb` with the AWS SDK v2, there is no need to change anything about your existing runtime code. It is purely type definitions, so you only need to cast an instance of `AWS.DynamoDB` to the `TypeSafeDynamoDBv2` interface and use the client as normal, except now you can enjoy a dynamic, type-safe experience in your IDE instead. 24 | 25 | ```ts 26 | import { DynamoDB } from "aws-sdk"; 27 | 28 | const client = new DynamoDB(); 29 | ``` 30 | 31 | Start by declaring a standard TypeScript interface which describes the structure of data in your DynamoDB Table: 32 | 33 | ```ts 34 | interface Record { 35 | key: string; 36 | sort: number; 37 | attribute: string; 38 | // all types are allowed, such as recursive nested types 39 | records?: Record[]; 40 | } 41 | ``` 42 | 43 | Then, cast the `DynamoDB` client instance to `TypeSafeDynamoDB`; 44 | 45 | ```ts 46 | import { TypeSafeDynamoDBv2 } from "typesafe-dynamodb/lib/client-v2"; 47 | 48 | const typesafeClient: TypeSafeDynamoDBv2 = client; 49 | ``` 50 | 51 | `"key"` is the name of the Hash Key attribute, and `"sort"` is the name of the Range Key attribute. 52 | 53 | Finally, use the client as you normally would, except now with intelligent type hints and validations. 54 | 55 | ### AWS SDK v3 56 | 57 | #### Option 1 - DynamoDB (similar to SDK v2) 58 | 59 | `DynamoDB` is an almost identical implementation to the AWS SDK v2, except with minor changes such as returning a `Promise` by default. It is a convenient way of using the DynamoDB API, except it is not optimized for tree-shaking (for that, see Option 2). 60 | 61 | To override the types, follow a similar method to v2, except by importing TypeSafeDynamoDBv3 (instead of v2): 62 | 63 | ```ts 64 | import { DynamoDB } from "@aws-sdk/client-dynamodb"; 65 | import { TypeSafeDynamoDBv3 } from "typesafe-dynamodb/lib/client-v3"; 66 | 67 | const client = new DynamoDB({..}); 68 | const typesafeClient: TypeSafeDynamoDBv3 = client; 69 | ``` 70 | 71 | #### Option 2 - DynamoDBClient (a Command-Response interface optimized for tree-shaking) 72 | 73 | `DynamoDBClient` is a generic interface with a single method, `send`. To invoke an API, call `send` with an instance of the API's corresponding `Command`. 74 | 75 | This option is designed for optimal tree-shaking when bundling code, ensuring that the bundle only includes code for the APIs your application uses. 76 | 77 | For this option, type-safety is achieved by declaring your own commands and then calling the standard `DynamoDBClient`: 78 | 79 | ```ts 80 | interface MyType { 81 | key: string; 82 | sort: number; 83 | list: string[]; 84 | } 85 | 86 | const client = new DynamoDBClient({}); 87 | 88 | const GetMyTypeCommand = TypeSafeGetItemCommand(); 89 | 90 | await client.send( 91 | new GetMyTypeCommand({ 92 | .. 93 | }) 94 | ); 95 | ``` 96 | 97 | ### Document Client 98 | 99 | Both the AWS SDK v2 and v3 provide a javascript-friendly interface called the `DocumentClient`. Instead of using the AttributeValue format, such as `{ S: "hello" }` or `{ N: "123" }`, the `DocumentClient` enables you to use native javascript types, e.g. `"hello"` or `123`. 100 | 101 | #### AWS SDK V2 102 | 103 | For the SDK V2 client, cast it to `TypeSafeDocumentClientV2`. 104 | 105 | See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html 106 | 107 | ```ts 108 | import { DynamoDB } from "aws-sdk"; 109 | import { TypeSafeDocumentClientV2 } from "typesafe-dynamodb/lib/document-client-v2"; 110 | 111 | const table = new DynamoDB.DocumentClient() as TypeSafeDocumentClientV2< 112 | MyItem, 113 | "pk", 114 | "sk" 115 | >; 116 | ``` 117 | 118 | #### AWS SDK V3 119 | 120 | When defining your Command types, use the corresponding `TypeSafe*DocumentCommand` type, for example `TypeSafeGetDocumentCommand` instead of `TypeSafeGetItemCommand`: 121 | 122 | - GetItem - `TypeSafeGetDocumentCommand` 123 | - PutItem - `TypeSafePutDocumentCommand` 124 | - DeleteItem - `TypeSafeDeleteDocumentCommand` 125 | - UpdateItem - `TypeSafeUpdateDocumentCommand` 126 | - Query - `TypeSafeQueryDocumentCommand` 127 | - Scan - `TypeSafeScanDocumentCommand` 128 | 129 | See: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_lib_dynamodb.html 130 | 131 | ```ts 132 | import { JsonFormat } from "typesafe-dynamodb"; 133 | import { TypeSafeGetDocumentCommand } from "typesafe-dynamodb/lib/get-document-command"; 134 | 135 | const MyGetItemCommand = TypeSafeGetDocumentCommand(); 136 | ``` 137 | 138 | For the SDK V3 client, cast it to `TypeSafeDynamoDBv3`. 139 | 140 | ```ts 141 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 142 | import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; 143 | import { TypeSafeDocumentClientV3 } from "typesafe-dynamodb/lib/document-client-v3"; 144 | 145 | const client = new DynamoDBClient({}); 146 | 147 | const docClient = DynamoDBDocumentClient.from( 148 | client 149 | ) as TypeSafeDocumentClientV3; 150 | ``` 151 | 152 | And then call `.send` with an instance of your TypeSafe Command: 153 | 154 | ```ts 155 | docClient.send(new MyGetItemCommand({ 156 | .. 157 | })); 158 | ``` 159 | 160 | ## Features 161 | 162 | ### Type-aware Input and Output 163 | 164 | The type of the `Key` is derived from the `Record` type. 165 | 166 | ![typesafe GetItem Key](img/get-item.gif) 167 | 168 | Same for the `Item` in the response: 169 | 170 | ![typesafe GetItemOutput Item](img/get-item-response.gif) 171 | 172 | ### Single Table Design 173 | 174 | Below are two `interface` declarations, representing two types of data stored in a single DynamoDB table - `User` and `Order`. Single table design in DynamoDB is achieved by creating "composite keys", e.g. `USER#${UserID}`. In TypeScript, we use template literal types to encode this in the Type System. 175 | 176 | ```ts 177 | interface User { 178 | PK: `USER#${UserID}`; 179 | SK: `#PROFILE#${UserID}`; 180 | Username: string; 181 | FullName: string; 182 | Email: string; 183 | CreatedAt: Date; 184 | Address: string; 185 | } 186 | 187 | interface Order< 188 | UserID extends string = string, 189 | OrderID extends string = string 190 | > { 191 | PK: `USER#${UserID}`; 192 | SK: `ORDER#${OrderID}`; 193 | Username: string; 194 | OrderID: string; 195 | Status: "PLACED" | "SHIPPED"; 196 | CreatedAt: Date; 197 | Address: string; 198 | } 199 | ``` 200 | 201 | With these two types defined, you can now use a union type to declare a `TypeSafeDynamoDB` instance aware of the two types of data in your tables: 202 | 203 | ```ts 204 | const client: TypeSafeDynamoDB; 205 | ``` 206 | 207 | When making calls such as `getItem`, TypeScript will narrow the returned data type to the corresponding type based on the structure of the `Key` in your request: 208 | 209 | ![narrowed getItem](img/get-order.png) 210 | 211 | ### Type-Safe DynamoDBStreamEvent 212 | 213 | Leverage your data types in Lambda Functions attached to the DynamoDB Table Stream: 214 | 215 | ```ts 216 | import { DynamoDBStreamEvent } from "typesafe-dynamodb/lib/stream-event"; 217 | 218 | export async function handle( 219 | event: DynamoDBStreamEvent 220 | ) { 221 | .. 222 | } 223 | ``` 224 | 225 | The event's type is derived from the data type and the the `StreamViewType`, e.g. `"NEW_IMAGE" | "OLD_IMAGE" | "KEYS_ONLY" | "NEW_AND_OLD_IMAGES"`. 226 | 227 | ### Filter result with ProjectionExpression 228 | 229 | The `ProjectionExpression` field is parsed and applied to filter the returned type of `getItem` and `query`. 230 | 231 | ![typesafe ProjectionExpression](img/get-item-projection.gif) 232 | 233 | ### Filter with AttributesToGet 234 | 235 | If you specify `AttributesToGet`, then the returned type only contains those properties. 236 | 237 | ![typesafe AttributesToGet](img/get-item-attributes.gif) 238 | 239 | ### Validate ExpressionAttributeNames and ExpressionAttributeValues 240 | 241 | If you add a `ConditionExpression` in `putItem`, you will be prompted for any `#name` or `:valu` placeholders: 242 | 243 | ![typesafe putItem ConditionExpression](img/put-item-expression.gif) 244 | 245 | Same is true for a `query`: 246 | 247 | ![typesafe query KeyConditionExpression and Filter](img/query-expression.gif) 248 | 249 | ### Marshall a JS Object to an AttributeMap 250 | 251 | A better type definition `@aws-sdk/util-dynamodb`'s `marshall` function is provided which maintains the typing information of the value passed to it and also adapts the output type based on the input `marshallOptions` 252 | 253 | Given an object literal: 254 | 255 | ```ts 256 | const myObject = { 257 | key: "key", 258 | sort: 123, 259 | binary: new Uint16Array([1]), 260 | buffer: Buffer.from("buffer", "utf8"), 261 | optional: 456, 262 | list: ["hello", "world"], 263 | record: { 264 | key: "nested key", 265 | sort: 789, 266 | }, 267 | } as const; 268 | ``` 269 | 270 | Call the `marshall` function to convert it to an AttributeMap which maintains the exact structure in the type system: 271 | 272 | ```ts 273 | import { marshall } from "typesafe-dynamodb/lib/marshall"; 274 | // marshall the above JS object to its corresponding AttributeMap 275 | const marshalled = marshall(myObject) 276 | 277 | // typing information is carried across exactly, including literal types 278 | const marshalled: { 279 | readonly key: S<"key">; 280 | readonly sort: N<123>; 281 | readonly binary: B; 282 | readonly buffer: B; 283 | readonly optional: N<456>; 284 | readonly list: L, S<"world">]>; 285 | readonly record: M<...>; 286 | } 287 | ``` 288 | 289 | ### Unmarshall an AttributeMap back to a JS Object 290 | 291 | A better type definition `@aws-sdk/util-dynamodb`'s `unmarshall` function is provided which maintains the typing information of the value passed to it and also adapts the output type based on the input `unmarshallOptions`. 292 | 293 | ```ts 294 | import { unmarshall } from "typesafe-dynamodb/lib/marshall"; 295 | 296 | // unmarshall the AttributeMap back into the original object 297 | const unmarshalled = unmarshall(marshalled); 298 | 299 | // it maintains the literal typing information (as much as possible) 300 | const unmarshalled: { 301 | readonly key: "key"; 302 | readonly sort: 123; 303 | readonly binary: NativeBinaryAttribute; 304 | readonly buffer: NativeBinaryAttribute; 305 | readonly optional: 456; 306 | readonly list: readonly [...]; 307 | readonly record: Unmarshall<...>; 308 | } 309 | ``` 310 | 311 | If you specify `{wrapNumbers: true}`, then all `number` types will be wrapped as `{ value: string }`: 312 | 313 | ```ts 314 | const unmarshalled = unmarshall(marshalled, { 315 | wrapNumbers: true, 316 | }); 317 | 318 | // numbers are wrapped in { value: string } because of `wrappedNumbers: true` 319 | unmarshalled.sort.value; // string 320 | 321 | // it maintains the literal typing information (as much as possible) 322 | const unmarshalled: { 323 | readonly key: "key"; 324 | // notice how the number is wrapped in the `NumberValue` type? 325 | // this is because `wrapNumbers: true` 326 | readonly sort: NumberValue<123>; 327 | readonly binary: NativeBinaryAttribute; 328 | readonly buffer: NativeBinaryAttribute; 329 | readonly optional: NumberValue<...>; 330 | readonly list: readonly [...]; 331 | readonly record: Unmarshall<...>; 332 | }; 333 | ``` 334 | -------------------------------------------------------------------------------- /img/get-item-attributes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/get-item-attributes.gif -------------------------------------------------------------------------------- /img/get-item-projection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/get-item-projection.gif -------------------------------------------------------------------------------- /img/get-item-response.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/get-item-response.gif -------------------------------------------------------------------------------- /img/get-item.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/get-item.gif -------------------------------------------------------------------------------- /img/get-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/get-order.png -------------------------------------------------------------------------------- /img/put-item-expression.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/put-item-expression.gif -------------------------------------------------------------------------------- /img/query-expression.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sam-goodwin/typesafe-dynamodb/fed57ea6d79ca195648baa5f2e32cf50a3c6279b/img/query-expression.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typesafe-dynamodb", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/sam-goodwin/typesafe-dynamodb" 6 | }, 7 | "scripts": { 8 | "build": "npx projen build", 9 | "bump": "npx projen bump", 10 | "clobber": "npx projen clobber", 11 | "compile": "npx projen compile", 12 | "default": "npx projen default", 13 | "eject": "npx projen eject", 14 | "eslint": "npx projen eslint", 15 | "package": "npx projen package", 16 | "post-compile": "npx projen post-compile", 17 | "post-upgrade": "npx projen post-upgrade", 18 | "pre-compile": "npx projen pre-compile", 19 | "release": "npx projen release", 20 | "test": "npx projen test", 21 | "test:watch": "npx projen test:watch", 22 | "unbump": "npx projen unbump", 23 | "upgrade": "npx projen upgrade", 24 | "watch": "npx projen watch", 25 | "projen": "npx projen" 26 | }, 27 | "devDependencies": { 28 | "@aws-sdk/client-dynamodb": "3.669.0", 29 | "@aws-sdk/lib-dynamodb": "3.669.0", 30 | "@aws-sdk/types": "3.664.0", 31 | "@aws-sdk/util-dynamodb": "3.669.0", 32 | "@smithy/smithy-client": "3.4.0", 33 | "@types/aws-lambda": "8.10.145", 34 | "@types/jest": "^27.5.2", 35 | "@types/node": "^12", 36 | "@typescript-eslint/eslint-plugin": "^7", 37 | "@typescript-eslint/parser": "^7", 38 | "aws-sdk": "2.1691.0", 39 | "commit-and-tag-version": "^12", 40 | "constructs": "^10.0.0", 41 | "eslint": "^8", 42 | "eslint-import-resolver-typescript": "^2.7.1", 43 | "eslint-plugin-import": "^2.28.1", 44 | "jest": "^27.5.1", 45 | "jest-junit": "^15", 46 | "projen": "^0.88.1", 47 | "ts-jest": "^27.1.5", 48 | "typescript": "latest" 49 | }, 50 | "peerDependencies": { 51 | "@aws-sdk/client-dynamodb": "^3.669.0", 52 | "@aws-sdk/lib-dynamodb": "^3.669.0", 53 | "@aws-sdk/types": "^3.664.0", 54 | "@aws-sdk/util-dynamodb": "^3.669.0", 55 | "@smithy/smithy-client": "^3.4.0", 56 | "@types/aws-lambda": "^8.10.145", 57 | "aws-sdk": "^2.1691.0" 58 | }, 59 | "main": "lib/index.js", 60 | "license": "Apache-2.0", 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "version": "0.0.0", 65 | "jest": { 66 | "coverageProvider": "v8", 67 | "testMatch": [ 68 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 69 | "/@(src|test)/**/__tests__/**/*.ts?(x)" 70 | ], 71 | "clearMocks": true, 72 | "collectCoverage": true, 73 | "coverageReporters": [ 74 | "json", 75 | "lcov", 76 | "clover", 77 | "cobertura", 78 | "text" 79 | ], 80 | "coverageDirectory": "coverage", 81 | "coveragePathIgnorePatterns": [ 82 | "/node_modules/" 83 | ], 84 | "testPathIgnorePatterns": [ 85 | "/node_modules/" 86 | ], 87 | "watchPathIgnorePatterns": [ 88 | "/node_modules/" 89 | ], 90 | "reporters": [ 91 | "default", 92 | [ 93 | "jest-junit", 94 | { 95 | "outputDirectory": "test-reports" 96 | } 97 | ] 98 | ], 99 | "preset": "ts-jest", 100 | "globals": { 101 | "ts-jest": { 102 | "tsconfig": "tsconfig.dev.json" 103 | } 104 | } 105 | }, 106 | "types": "lib/index.d.ts", 107 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 108 | } 109 | -------------------------------------------------------------------------------- /src/attribute-value.ts: -------------------------------------------------------------------------------- 1 | export type AttributeValue = 2 | | undefined 3 | | S 4 | | N 5 | | B 6 | | BOOL 7 | | NULL 8 | | L> 9 | | M>; 10 | 11 | export type AttributeMap = Record; 12 | 13 | export type NativeBinaryAttribute = 14 | | ArrayBuffer 15 | | BigInt64Array 16 | | BigUint64Array 17 | | Buffer 18 | | DataView 19 | | Float32Array 20 | | Float64Array 21 | | Int16Array 22 | | Int32Array 23 | | Int8Array 24 | | Uint16Array 25 | | Uint32Array 26 | | Uint8Array 27 | | Uint8ClampedArray; 28 | 29 | export type DocumentValue = 30 | | undefined 31 | | null 32 | | boolean 33 | | number 34 | | string 35 | | DocumentValue[] 36 | | NativeBinaryAttribute 37 | | { 38 | [key: string]: DocumentValue; 39 | }; 40 | 41 | // @ts-ignore 42 | export type ToAttributeMap = ToAttributeValue["M"]; 43 | 44 | /** 45 | * Computes the JSON representation of an object, {@link T}. 46 | */ 47 | export type ToAttributeValue = T extends undefined 48 | ? undefined 49 | : T extends null 50 | ? NULL 51 | : T extends boolean 52 | ? BOOL 53 | : T extends string 54 | ? S 55 | : T extends number 56 | ? N 57 | : // this behavior is not defined by https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html 58 | // should it be a number of string? 59 | T extends Date 60 | ? N 61 | : T extends NativeBinaryAttribute 62 | ? B 63 | : T extends ArrayLike 64 | ? L<{ 65 | [i in keyof T]: i extends "length" ? T[i] : ToAttributeValue; 66 | }> 67 | : M<{ 68 | [prop in keyof T]: ToAttributeValue; 69 | }>; 70 | 71 | export function isS(a: any): a is S { 72 | return a !== undefined && "S" in a; 73 | } 74 | 75 | export interface S { 76 | S: S; 77 | } 78 | 79 | export function isB(a: any): a is B { 80 | return a !== undefined && "B" in a; 81 | } 82 | 83 | export interface B { 84 | B: Buffer; 85 | } 86 | 87 | export function isBOOL(a: any): a is BOOL { 88 | return a !== undefined && "BOOL" in a; 89 | } 90 | 91 | export interface BOOL { 92 | BOOL: B; 93 | } 94 | 95 | export function isM(a: any): a is M { 96 | return a !== undefined && "M" in a; 97 | } 98 | 99 | export interface M< 100 | M extends Record = Record 101 | > { 102 | M: M; 103 | } 104 | 105 | export function isN(a: any): a is N { 106 | return a !== undefined && "N" in a; 107 | } 108 | 109 | export interface N { 110 | N: `${N}`; 111 | } 112 | 113 | export function isNULL(a: any): a is NULL { 114 | return a !== undefined && "NULL" in a; 115 | } 116 | 117 | export interface NULL { 118 | NULL: boolean; 119 | } 120 | 121 | export function isL(a: any): a is L { 122 | return a !== undefined && "L" in a; 123 | } 124 | 125 | export interface L< 126 | L extends ArrayLike = ArrayLike 127 | > { 128 | L: L; 129 | } 130 | -------------------------------------------------------------------------------- /src/callback.ts: -------------------------------------------------------------------------------- 1 | import type { AWSError } from "aws-sdk"; 2 | 3 | export type Callback = (err: Err, data: Data) => void; 4 | -------------------------------------------------------------------------------- /src/client-v2.ts: -------------------------------------------------------------------------------- 1 | import type { AWSError, DynamoDB, Request } from "aws-sdk"; 2 | import { Callback } from "./callback"; 3 | import { DeleteItemInput, DeleteItemOutput } from "./delete-item"; 4 | import { JsonFormat } from "./json-format"; 5 | import { GetItemInput, GetItemOutput } from "./get-item"; 6 | import { TableKey } from "./key"; 7 | import { PutItemInput, PutItemOutput } from "./put-item"; 8 | import { QueryInput, QueryOutput } from "./query"; 9 | import { ScanInput, ScanOutput } from "./scan"; 10 | import { UpdateItemInput, UpdateItemOutput } from "./update-item"; 11 | 12 | export interface TypeSafeDynamoDBv2< 13 | Item extends object, 14 | PartitionKey extends keyof Item, 15 | RangeKey extends keyof Item | undefined = undefined 16 | > extends Omit< 17 | DynamoDB, 18 | "getItem" | "deleteItem" | "putItem" | "query" | "scan" | "updateItem" 19 | > { 20 | getItem< 21 | Key extends TableKey< 22 | Item, 23 | PartitionKey, 24 | RangeKey, 25 | JsonFormat.AttributeValue 26 | >, 27 | AttributesToGet extends keyof Item | undefined = undefined, 28 | ProjectionExpression extends string | undefined = undefined 29 | >( 30 | params: GetItemInput< 31 | Item, 32 | PartitionKey, 33 | RangeKey, 34 | Key, 35 | AttributesToGet, 36 | ProjectionExpression, 37 | JsonFormat.AttributeValue 38 | >, 39 | callback?: Callback< 40 | GetItemOutput< 41 | Item, 42 | PartitionKey, 43 | RangeKey, 44 | Key, 45 | AttributesToGet, 46 | ProjectionExpression, 47 | JsonFormat.AttributeValue 48 | >, 49 | AWSError 50 | > 51 | ): Request< 52 | GetItemOutput< 53 | Item, 54 | PartitionKey, 55 | RangeKey, 56 | Key, 57 | AttributesToGet, 58 | ProjectionExpression, 59 | JsonFormat.AttributeValue 60 | >, 61 | AWSError 62 | >; 63 | 64 | deleteItem< 65 | Key extends TableKey< 66 | Item, 67 | PartitionKey, 68 | RangeKey, 69 | JsonFormat.AttributeValue 70 | >, 71 | ConditionExpression extends string | undefined, 72 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 73 | >( 74 | params: DeleteItemInput< 75 | Item, 76 | PartitionKey, 77 | RangeKey, 78 | Key, 79 | ConditionExpression, 80 | ReturnValue, 81 | JsonFormat.AttributeValue 82 | >, 83 | callback?: Callback< 84 | DeleteItemOutput, 85 | AWSError 86 | > 87 | ): Request< 88 | DeleteItemOutput, 89 | AWSError 90 | >; 91 | 92 | putItem< 93 | ConditionExpression extends string | undefined, 94 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 95 | >( 96 | params: PutItemInput< 97 | Item, 98 | ConditionExpression, 99 | ReturnValue, 100 | JsonFormat.AttributeValue 101 | >, 102 | callback?: Callback< 103 | PutItemOutput, 104 | AWSError 105 | > 106 | ): Request< 107 | PutItemOutput, 108 | AWSError 109 | >; 110 | 111 | updateItem< 112 | Key extends TableKey< 113 | Item, 114 | PartitionKey, 115 | RangeKey, 116 | JsonFormat.AttributeValue 117 | >, 118 | UpdateExpression extends string, 119 | ConditionExpression extends string | undefined, 120 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 121 | >( 122 | params: UpdateItemInput< 123 | Item, 124 | PartitionKey, 125 | RangeKey, 126 | Key, 127 | UpdateExpression, 128 | ConditionExpression, 129 | ReturnValue, 130 | JsonFormat.AttributeValue 131 | >, 132 | callback?: Callback< 133 | UpdateItemOutput< 134 | Item, 135 | PartitionKey, 136 | RangeKey, 137 | Key, 138 | ReturnValue, 139 | JsonFormat.AttributeValue 140 | >, 141 | AWSError 142 | > 143 | ): Request< 144 | UpdateItemOutput< 145 | Item, 146 | PartitionKey, 147 | RangeKey, 148 | Key, 149 | ReturnValue, 150 | JsonFormat.AttributeValue 151 | >, 152 | AWSError 153 | >; 154 | 155 | query< 156 | KeyConditionExpression extends string | undefined = undefined, 157 | FilterExpression extends string | undefined = undefined, 158 | ProjectionExpression extends string | undefined = undefined, 159 | AttributesToGet extends keyof Item | undefined = undefined 160 | >( 161 | params: QueryInput< 162 | Item, 163 | KeyConditionExpression, 164 | FilterExpression, 165 | ProjectionExpression, 166 | AttributesToGet, 167 | JsonFormat.AttributeValue 168 | >, 169 | callback?: Callback< 170 | QueryOutput, 171 | AWSError 172 | > 173 | ): Request< 174 | QueryOutput, 175 | AWSError 176 | >; 177 | 178 | scan< 179 | FilterExpression extends string | undefined = undefined, 180 | ProjectionExpression extends string | undefined = undefined, 181 | AttributesToGet extends keyof Item | undefined = undefined 182 | >( 183 | params: ScanInput< 184 | Item, 185 | FilterExpression, 186 | ProjectionExpression, 187 | AttributesToGet, 188 | JsonFormat.AttributeValue 189 | >, 190 | callback?: Callback< 191 | ScanOutput, 192 | AWSError 193 | > 194 | ): Request< 195 | ScanOutput, 196 | AWSError 197 | >; 198 | } 199 | -------------------------------------------------------------------------------- /src/client-v3.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DynamoDB, 3 | ReturnValue as DynamoDBReturnValue, 4 | } from "@aws-sdk/client-dynamodb"; 5 | import { MetadataBearer } from "@aws-sdk/types"; 6 | import { ProjectionExpression } from "aws-sdk/clients/dynamodb"; 7 | import { Callback } from "./callback"; 8 | import { DeleteItemInput, DeleteItemOutput } from "./delete-item"; 9 | import { JsonFormat } from "./json-format"; 10 | import { GetItemInput, GetItemOutput } from "./get-item"; 11 | import { TableKey } from "./key"; 12 | import { PutItemInput, PutItemOutput } from "./put-item"; 13 | import { QueryInput, QueryOutput } from "./query"; 14 | import { ScanInput, ScanOutput } from "./scan"; 15 | import { UpdateItemInput, UpdateItemOutput } from "./update-item"; 16 | 17 | export interface TypeSafeDynamoDBv3< 18 | Item extends object, 19 | PartitionKey extends keyof Item, 20 | RangeKey extends keyof Item | undefined = undefined 21 | > extends Omit< 22 | DynamoDB, 23 | "getItem" | "deleteItem" | "putItem" | "updateItem" | "query" | "scan" 24 | > { 25 | getItem< 26 | Key extends TableKey< 27 | Item, 28 | PartitionKey, 29 | RangeKey, 30 | JsonFormat.AttributeValue 31 | >, 32 | AttributesToGet extends keyof Item | undefined = undefined, 33 | ProjectionExpression extends string | undefined = undefined 34 | >( 35 | params: GetItemInput< 36 | Item, 37 | PartitionKey, 38 | RangeKey, 39 | Key, 40 | AttributesToGet, 41 | ProjectionExpression, 42 | JsonFormat.AttributeValue 43 | > 44 | ): Promise< 45 | GetItemOutput< 46 | Item, 47 | PartitionKey, 48 | RangeKey, 49 | Key, 50 | AttributesToGet, 51 | ProjectionExpression, 52 | JsonFormat.AttributeValue 53 | > & 54 | MetadataBearer 55 | >; 56 | 57 | getItem< 58 | Key extends TableKey< 59 | Item, 60 | PartitionKey, 61 | RangeKey, 62 | JsonFormat.AttributeValue 63 | >, 64 | AttributesToGet extends keyof Item | undefined = undefined, 65 | ProjectionExpression extends string | undefined = undefined 66 | >( 67 | params: GetItemInput< 68 | Item, 69 | PartitionKey, 70 | RangeKey, 71 | Key, 72 | AttributesToGet, 73 | ProjectionExpression, 74 | JsonFormat.AttributeValue 75 | >, 76 | callback: Callback< 77 | GetItemOutput< 78 | Item, 79 | PartitionKey, 80 | RangeKey, 81 | Key, 82 | AttributesToGet, 83 | ProjectionExpression, 84 | JsonFormat.AttributeValue 85 | > & 86 | MetadataBearer, 87 | any 88 | > 89 | ): void; 90 | 91 | deleteItem< 92 | Key extends TableKey< 93 | Item, 94 | PartitionKey, 95 | RangeKey, 96 | JsonFormat.AttributeValue 97 | >, 98 | ConditionExpression extends string | undefined, 99 | ReturnValue extends DynamoDBReturnValue = "NONE" 100 | >( 101 | params: DeleteItemInput< 102 | Item, 103 | PartitionKey, 104 | RangeKey, 105 | Key, 106 | ConditionExpression, 107 | ReturnValue, 108 | JsonFormat.AttributeValue 109 | > 110 | ): Promise< 111 | DeleteItemOutput & 112 | MetadataBearer 113 | >; 114 | 115 | deleteItem< 116 | Key extends TableKey< 117 | Item, 118 | PartitionKey, 119 | RangeKey, 120 | JsonFormat.AttributeValue 121 | >, 122 | ConditionExpression extends string | undefined, 123 | ReturnValue extends DynamoDBReturnValue = "NONE" 124 | >( 125 | params: DeleteItemInput< 126 | Item, 127 | PartitionKey, 128 | RangeKey, 129 | Key, 130 | ConditionExpression, 131 | ReturnValue, 132 | JsonFormat.AttributeValue 133 | >, 134 | callback: Callback< 135 | DeleteItemOutput & 136 | MetadataBearer, 137 | any 138 | > 139 | ): void; 140 | 141 | putItem< 142 | ConditionExpression extends string | undefined, 143 | ReturnValue extends DynamoDBReturnValue = "NONE" 144 | >( 145 | params: PutItemInput< 146 | Item, 147 | ConditionExpression, 148 | ReturnValue, 149 | JsonFormat.AttributeValue 150 | > 151 | ): Promise< 152 | PutItemOutput & MetadataBearer 153 | >; 154 | 155 | putItem< 156 | ConditionExpression extends string | undefined, 157 | ReturnValue extends DynamoDBReturnValue = "NONE" 158 | >( 159 | params: PutItemInput< 160 | Item, 161 | ConditionExpression, 162 | ReturnValue, 163 | JsonFormat.AttributeValue 164 | >, 165 | callback: Callback< 166 | PutItemOutput & 167 | MetadataBearer, 168 | any 169 | > 170 | ): void; 171 | 172 | updateItem< 173 | Key extends TableKey< 174 | Item, 175 | PartitionKey, 176 | RangeKey, 177 | JsonFormat.AttributeValue 178 | >, 179 | UpdateExpression extends string, 180 | ConditionExpression extends string | undefined, 181 | ReturnValue extends DynamoDBReturnValue = "NONE" 182 | >( 183 | params: UpdateItemInput< 184 | Item, 185 | PartitionKey, 186 | RangeKey, 187 | Key, 188 | UpdateExpression, 189 | ConditionExpression, 190 | ReturnValue, 191 | JsonFormat.AttributeValue 192 | > 193 | ): Promise< 194 | UpdateItemOutput< 195 | Item, 196 | PartitionKey, 197 | RangeKey, 198 | Key, 199 | ReturnValue, 200 | JsonFormat.AttributeValue 201 | > & 202 | MetadataBearer 203 | >; 204 | 205 | updateItem< 206 | Key extends TableKey< 207 | Item, 208 | PartitionKey, 209 | RangeKey, 210 | JsonFormat.AttributeValue 211 | >, 212 | UpdateExpression extends string, 213 | ConditionExpression extends string | undefined, 214 | ReturnValue extends DynamoDBReturnValue = "NONE" 215 | >( 216 | params: UpdateItemInput< 217 | Item, 218 | PartitionKey, 219 | RangeKey, 220 | Key, 221 | UpdateExpression, 222 | ConditionExpression, 223 | ReturnValue, 224 | JsonFormat.AttributeValue 225 | >, 226 | callback?: Callback< 227 | UpdateItemOutput< 228 | Item, 229 | PartitionKey, 230 | RangeKey, 231 | Key, 232 | ReturnValue, 233 | JsonFormat.AttributeValue 234 | > & 235 | MetadataBearer, 236 | any 237 | > 238 | ): void; 239 | 240 | query< 241 | KeyConditionExpression extends string | undefined = undefined, 242 | FilterExpression extends string | undefined = undefined, 243 | ProjectionExpression extends string | undefined = undefined, 244 | AttributesToGet extends keyof Item | undefined = undefined 245 | >( 246 | params: QueryInput< 247 | Item, 248 | KeyConditionExpression, 249 | FilterExpression, 250 | ProjectionExpression, 251 | AttributesToGet, 252 | JsonFormat.AttributeValue 253 | > 254 | ): Promise< 255 | QueryOutput & 256 | MetadataBearer 257 | >; 258 | 259 | query< 260 | KeyConditionExpression extends string | undefined = undefined, 261 | FilterExpression extends string | undefined = undefined, 262 | AttributesToGet extends keyof Item | undefined = undefined 263 | >( 264 | params: QueryInput< 265 | Item, 266 | KeyConditionExpression, 267 | FilterExpression, 268 | ProjectionExpression, 269 | AttributesToGet, 270 | JsonFormat.AttributeValue 271 | >, 272 | callback: Callback< 273 | QueryOutput & 274 | MetadataBearer, 275 | any 276 | > 277 | ): void; 278 | 279 | scan< 280 | FilterExpression extends string | undefined = undefined, 281 | ProjectionExpression extends string | undefined = undefined, 282 | AttributesToGet extends keyof Item | undefined = undefined 283 | >( 284 | params: ScanInput< 285 | Item, 286 | FilterExpression, 287 | ProjectionExpression, 288 | AttributesToGet, 289 | JsonFormat.Document 290 | > 291 | ): Promise< 292 | ScanOutput & 293 | MetadataBearer 294 | >; 295 | 296 | scan< 297 | FilterExpression extends string | undefined = undefined, 298 | ProjectionExpression extends string | undefined = undefined, 299 | AttributesToGet extends keyof Item | undefined = undefined 300 | >( 301 | params: ScanInput< 302 | Item, 303 | FilterExpression, 304 | ProjectionExpression, 305 | AttributesToGet, 306 | JsonFormat.AttributeValue 307 | >, 308 | callback?: Callback< 309 | ScanOutput & 310 | MetadataBearer, 311 | any 312 | > 313 | ): void; 314 | } 315 | -------------------------------------------------------------------------------- /src/create-set.ts: -------------------------------------------------------------------------------- 1 | import { NativeBinaryAttribute } from "./attribute-value"; 2 | 3 | export interface CreateSet { 4 | /** 5 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#createSet-property 6 | */ 7 | createSet( 8 | list: T 9 | ): { 10 | type: T[number] extends string 11 | ? "String" 12 | : T[number] extends number 13 | ? "Number" 14 | : T[number] extends NativeBinaryAttribute 15 | ? "Binary" 16 | : never; 17 | values: T; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/delete-document-command.ts: -------------------------------------------------------------------------------- 1 | import { DeleteCommand as _DeleteCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { DeleteCommand } from "./delete-item"; 3 | import type { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeDeleteDocumentCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): DeleteCommand { 10 | return _DeleteCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/delete-item-command.ts: -------------------------------------------------------------------------------- 1 | import { DeleteItemCommand as _DeleteItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import type { DeleteCommand } from "./delete-item"; 3 | import type { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeDeleteItemCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): DeleteCommand { 10 | return _DeleteItemCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/delete-item.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDB } from "aws-sdk"; 2 | import type { 3 | ExpressionAttributeNames, 4 | ExpressionAttributeValues, 5 | } from "./expression-attributes"; 6 | import type { FormatObject, JsonFormat } from "./json-format"; 7 | import type { TableKey } from "./key"; 8 | import type { 9 | DynamoDBClientResolvedConfig, 10 | ReturnValue as DynamoDBReturnValue, 11 | } from "@aws-sdk/client-dynamodb"; 12 | import type { Command } from "@smithy/smithy-client"; 13 | import type { MetadataBearer } from "@aws-sdk/types"; 14 | 15 | export type DeleteItemInput< 16 | Item extends object, 17 | PartitionKey extends keyof Item, 18 | RangeKey extends keyof Item | undefined, 19 | Key extends TableKey, 20 | ConditionExpression extends string | undefined, 21 | ReturnValue extends DynamoDB.ReturnValue, 22 | Format extends JsonFormat 23 | > = Omit< 24 | DynamoDB.DeleteItemInput, 25 | | "ConditionExpression" 26 | | "ExpressionAttributeNames" 27 | | "ExpressionAttributeValues" 28 | | "Item" 29 | | "Key" 30 | | "ReturnValues" 31 | > & 32 | ExpressionAttributeNames & 33 | ExpressionAttributeValues & { 34 | Key: Key; 35 | ReturnValues?: ReturnValue; 36 | ConditionExpression?: ConditionExpression; 37 | }; 38 | 39 | export interface DeleteItemOutput< 40 | Item extends object, 41 | ReturnValue extends DynamoDB.ReturnValue, 42 | Format extends JsonFormat 43 | > extends Omit { 44 | Attributes?: "ALL_OLD" | "ALL_NEW" extends ReturnValue 45 | ? FormatObject 46 | : undefined | "NONE" extends ReturnValue 47 | ? undefined 48 | : "UPDATED_OLD" | "UPDATED_NEW" extends ReturnValue 49 | ? Partial> 50 | : Partial>; 51 | } 52 | 53 | export type DeleteCommand< 54 | Item extends object, 55 | PartitionKey extends keyof Item, 56 | RangeKey extends keyof Item | undefined, 57 | Format extends JsonFormat 58 | > = new < 59 | Key extends TableKey, 60 | const ConditionExpression extends string | undefined = undefined, 61 | const ReturnValue extends DynamoDBReturnValue = "NONE" 62 | >( 63 | input: DeleteItemInput< 64 | Item, 65 | PartitionKey, 66 | RangeKey, 67 | Key, 68 | ConditionExpression, 69 | ReturnValue, 70 | Format 71 | > 72 | ) => Command< 73 | DeleteItemInput< 74 | Item, 75 | PartitionKey, 76 | RangeKey, 77 | Key, 78 | ConditionExpression, 79 | ReturnValue, 80 | Format 81 | >, 82 | DeleteItemOutput & MetadataBearer, 83 | DynamoDBClientResolvedConfig 84 | > & { 85 | _brand: "DeleteItemCommand"; 86 | }; 87 | -------------------------------------------------------------------------------- /src/document-client-field-mappings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html 3 | */ 4 | export type ToDocumentClient< 5 | T extends { 6 | [k in string]: any; 7 | } 8 | > = { 9 | batchGet: T["batchGetItem"]; 10 | batchWrite: T["batchWriteItem"]; 11 | delete: T["deleteItem"]; 12 | get: T["getItem"]; 13 | put: T["putItem"]; 14 | query: T["query"]; 15 | scan: T["scan"]; 16 | transactGet: T["transactGetItems"]; 17 | transactWrite: T["transactWriteItems"]; 18 | update: T["updateItem"]; 19 | }; 20 | -------------------------------------------------------------------------------- /src/document-client-v2.ts: -------------------------------------------------------------------------------- 1 | import type { AWSError, DynamoDB, Request } from "aws-sdk"; 2 | import { Callback } from "./callback"; 3 | import { DeleteItemInput, DeleteItemOutput } from "./delete-item"; 4 | import { JsonFormat } from "./json-format"; 5 | import { GetItemInput, GetItemOutput } from "./get-item"; 6 | import { TableKey } from "./key"; 7 | import { PutItemInput, PutItemOutput } from "./put-item"; 8 | import { QueryInput, QueryOutput } from "./query"; 9 | import { ScanInput, ScanOutput } from "./scan"; 10 | import { UpdateItemInput, UpdateItemOutput } from "./update-item"; 11 | 12 | export interface TypeSafeDocumentClientV2< 13 | Item extends object, 14 | PartitionKey extends keyof Item, 15 | RangeKey extends keyof Item | undefined = undefined 16 | > extends Omit< 17 | DynamoDB.DocumentClient, 18 | "get" | "delete" | "put" | "query" | "scan" | "update" 19 | > { 20 | get< 21 | Key extends TableKey, 22 | AttributesToGet extends keyof Item | undefined = undefined, 23 | ProjectionExpression extends string | undefined = undefined 24 | >( 25 | params: GetItemInput< 26 | Item, 27 | PartitionKey, 28 | RangeKey, 29 | Key, 30 | AttributesToGet, 31 | ProjectionExpression, 32 | JsonFormat.Document 33 | >, 34 | callback?: Callback< 35 | GetItemOutput< 36 | Item, 37 | PartitionKey, 38 | RangeKey, 39 | Key, 40 | AttributesToGet, 41 | ProjectionExpression, 42 | JsonFormat.Document 43 | >, 44 | AWSError 45 | > 46 | ): Request< 47 | GetItemOutput< 48 | Item, 49 | PartitionKey, 50 | RangeKey, 51 | Key, 52 | AttributesToGet, 53 | ProjectionExpression, 54 | JsonFormat.Document 55 | >, 56 | AWSError 57 | >; 58 | 59 | delete< 60 | Key extends TableKey, 61 | ConditionExpression extends string | undefined, 62 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 63 | >( 64 | params: DeleteItemInput< 65 | Item, 66 | PartitionKey, 67 | RangeKey, 68 | Key, 69 | ConditionExpression, 70 | ReturnValue, 71 | JsonFormat.Document 72 | >, 73 | callback?: Callback< 74 | DeleteItemOutput, 75 | AWSError 76 | > 77 | ): Request< 78 | DeleteItemOutput, 79 | AWSError 80 | >; 81 | 82 | put< 83 | ConditionExpression extends string | undefined, 84 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 85 | >( 86 | params: PutItemInput< 87 | Item, 88 | ConditionExpression, 89 | ReturnValue, 90 | JsonFormat.Document 91 | >, 92 | callback?: Callback< 93 | PutItemOutput, 94 | AWSError 95 | > 96 | ): Request, AWSError>; 97 | 98 | update< 99 | Key extends TableKey, 100 | UpdateExpression extends string, 101 | ConditionExpression extends string | undefined, 102 | ReturnValue extends DynamoDB.ReturnValue = "NONE" 103 | >( 104 | params: UpdateItemInput< 105 | Item, 106 | PartitionKey, 107 | RangeKey, 108 | Key, 109 | UpdateExpression, 110 | ConditionExpression, 111 | ReturnValue, 112 | JsonFormat.Document 113 | >, 114 | callback?: Callback< 115 | UpdateItemOutput< 116 | Item, 117 | PartitionKey, 118 | RangeKey, 119 | Key, 120 | ReturnValue, 121 | JsonFormat.Document 122 | >, 123 | AWSError 124 | > 125 | ): Request< 126 | UpdateItemOutput< 127 | Item, 128 | PartitionKey, 129 | RangeKey, 130 | Key, 131 | ReturnValue, 132 | JsonFormat.Document 133 | >, 134 | AWSError 135 | >; 136 | 137 | query< 138 | KeyConditionExpression extends string | undefined = undefined, 139 | FilterExpression extends string | undefined = undefined, 140 | ProjectionExpression extends string | undefined = undefined, 141 | AttributesToGet extends keyof Item | undefined = undefined 142 | >( 143 | params: QueryInput< 144 | Item, 145 | KeyConditionExpression, 146 | FilterExpression, 147 | ProjectionExpression, 148 | AttributesToGet, 149 | JsonFormat.Document 150 | >, 151 | callback?: Callback< 152 | QueryOutput, 153 | AWSError 154 | > 155 | ): Request, AWSError>; 156 | 157 | scan< 158 | FilterExpression extends string | undefined = undefined, 159 | ProjectionExpression extends string | undefined = undefined, 160 | AttributesToGet extends keyof Item | undefined = undefined 161 | >( 162 | params: ScanInput< 163 | Item, 164 | FilterExpression, 165 | ProjectionExpression, 166 | AttributesToGet, 167 | JsonFormat.Document 168 | >, 169 | callback?: Callback< 170 | ScanOutput, 171 | AWSError 172 | > 173 | ): Request, AWSError>; 174 | } 175 | -------------------------------------------------------------------------------- /src/document-client-v3.ts: -------------------------------------------------------------------------------- 1 | import type { ReturnValue as DynamoDBReturnValue } from "@aws-sdk/client-dynamodb"; 2 | import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"; 3 | import { MetadataBearer } from "@aws-sdk/types"; 4 | import { ProjectionExpression } from "aws-sdk/clients/dynamodb"; 5 | import { Callback } from "./callback"; 6 | import { DeleteItemInput, DeleteItemOutput } from "./delete-item"; 7 | import { JsonFormat } from "./json-format"; 8 | import { GetItemInput, GetItemOutput } from "./get-item"; 9 | import { TableKey } from "./key"; 10 | import { PutItemInput, PutItemOutput } from "./put-item"; 11 | import { QueryInput, QueryOutput } from "./query"; 12 | import { ScanInput, ScanOutput } from "./scan"; 13 | import { UpdateItemInput, UpdateItemOutput } from "./update-item"; 14 | 15 | export interface TypeSafeDocumentClientV3< 16 | Item extends object, 17 | PartitionKey extends keyof Item, 18 | RangeKey extends keyof Item | undefined = undefined 19 | > extends Omit< 20 | DynamoDBDocument, 21 | "get" | "delete" | "put" | "update" | "query" | "scan" 22 | > { 23 | get< 24 | Key extends TableKey, 25 | AttributesToGet extends keyof Item | undefined = undefined, 26 | ProjectionExpression extends string | undefined = undefined 27 | >( 28 | params: GetItemInput< 29 | Item, 30 | PartitionKey, 31 | RangeKey, 32 | Key, 33 | AttributesToGet, 34 | ProjectionExpression, 35 | JsonFormat.Document 36 | > 37 | ): Promise< 38 | GetItemOutput< 39 | Item, 40 | PartitionKey, 41 | RangeKey, 42 | Key, 43 | AttributesToGet, 44 | ProjectionExpression, 45 | JsonFormat.Document 46 | > & 47 | MetadataBearer 48 | >; 49 | 50 | get< 51 | Key extends TableKey, 52 | AttributesToGet extends keyof Item | undefined = undefined, 53 | ProjectionExpression extends string | undefined = undefined 54 | >( 55 | params: GetItemInput< 56 | Item, 57 | PartitionKey, 58 | RangeKey, 59 | Key, 60 | AttributesToGet, 61 | ProjectionExpression, 62 | JsonFormat.Document 63 | >, 64 | callback: Callback< 65 | GetItemOutput< 66 | Item, 67 | PartitionKey, 68 | RangeKey, 69 | Key, 70 | AttributesToGet, 71 | ProjectionExpression, 72 | JsonFormat.Document 73 | >, 74 | any 75 | > 76 | ): void; 77 | 78 | delete< 79 | Key extends TableKey, 80 | ConditionExpression extends string | undefined, 81 | ReturnValue extends DynamoDBReturnValue = "NONE" 82 | >( 83 | params: DeleteItemInput< 84 | Item, 85 | PartitionKey, 86 | RangeKey, 87 | Key, 88 | ConditionExpression, 89 | ReturnValue, 90 | JsonFormat.Document 91 | > 92 | ): Promise< 93 | DeleteItemOutput & MetadataBearer 94 | >; 95 | 96 | delete< 97 | Key extends TableKey, 98 | ConditionExpression extends string | undefined, 99 | ReturnValue extends DynamoDBReturnValue = "NONE" 100 | >( 101 | params: DeleteItemInput< 102 | Item, 103 | PartitionKey, 104 | RangeKey, 105 | Key, 106 | ConditionExpression, 107 | ReturnValue, 108 | JsonFormat.Document 109 | >, 110 | callback: Callback< 111 | DeleteItemOutput & MetadataBearer, 112 | any 113 | > 114 | ): void; 115 | 116 | put< 117 | ConditionExpression extends string | undefined, 118 | ReturnValue extends DynamoDBReturnValue = "NONE" 119 | >( 120 | params: PutItemInput< 121 | Item, 122 | ConditionExpression, 123 | ReturnValue, 124 | JsonFormat.Document 125 | > 126 | ): Promise< 127 | PutItemOutput & MetadataBearer 128 | >; 129 | 130 | put< 131 | ConditionExpression extends string | undefined, 132 | ReturnValue extends DynamoDBReturnValue = "NONE" 133 | >( 134 | params: PutItemInput< 135 | Item, 136 | ConditionExpression, 137 | ReturnValue, 138 | JsonFormat.Document 139 | >, 140 | callback: Callback< 141 | PutItemOutput & MetadataBearer, 142 | any 143 | > 144 | ): void; 145 | 146 | update< 147 | Key extends TableKey, 148 | UpdateExpression extends string, 149 | ConditionExpression extends string | undefined, 150 | ReturnValue extends DynamoDBReturnValue = "NONE" 151 | >( 152 | params: UpdateItemInput< 153 | Item, 154 | PartitionKey, 155 | RangeKey, 156 | Key, 157 | UpdateExpression, 158 | ConditionExpression, 159 | ReturnValue, 160 | JsonFormat.Document 161 | > 162 | ): Promise< 163 | UpdateItemOutput< 164 | Item, 165 | PartitionKey, 166 | RangeKey, 167 | Key, 168 | ReturnValue, 169 | JsonFormat.Document 170 | > & 171 | MetadataBearer 172 | >; 173 | 174 | update< 175 | Key extends TableKey, 176 | UpdateExpression extends string, 177 | ConditionExpression extends string | undefined, 178 | ReturnValue extends DynamoDBReturnValue = "NONE" 179 | >( 180 | params: UpdateItemInput< 181 | Item, 182 | PartitionKey, 183 | RangeKey, 184 | Key, 185 | UpdateExpression, 186 | ConditionExpression, 187 | ReturnValue, 188 | JsonFormat.Document 189 | >, 190 | callback?: Callback< 191 | UpdateItemOutput< 192 | Item, 193 | PartitionKey, 194 | RangeKey, 195 | Key, 196 | ReturnValue, 197 | JsonFormat.Document 198 | > & 199 | MetadataBearer, 200 | any 201 | > 202 | ): void; 203 | 204 | query< 205 | KeyConditionExpression extends string | undefined = undefined, 206 | FilterExpression extends string | undefined = undefined, 207 | ProjectionExpression extends string | undefined = undefined, 208 | AttributesToGet extends keyof Item | undefined = undefined 209 | >( 210 | params: QueryInput< 211 | Item, 212 | KeyConditionExpression, 213 | FilterExpression, 214 | ProjectionExpression, 215 | AttributesToGet, 216 | JsonFormat.Document 217 | > 218 | ): Promise< 219 | QueryOutput & MetadataBearer 220 | >; 221 | 222 | query< 223 | KeyConditionExpression extends string | undefined = undefined, 224 | FilterExpression extends string | undefined = undefined, 225 | AttributesToGet extends keyof Item | undefined = undefined 226 | >( 227 | params: QueryInput< 228 | Item, 229 | KeyConditionExpression, 230 | FilterExpression, 231 | ProjectionExpression, 232 | AttributesToGet, 233 | JsonFormat.Document 234 | >, 235 | callback: Callback< 236 | QueryOutput & MetadataBearer, 237 | any 238 | > 239 | ): void; 240 | 241 | scan< 242 | FilterExpression extends string | undefined = undefined, 243 | ProjectionExpression extends string | undefined = undefined, 244 | AttributesToGet extends keyof Item | undefined = undefined 245 | >( 246 | params: ScanInput< 247 | Item, 248 | FilterExpression, 249 | ProjectionExpression, 250 | AttributesToGet, 251 | JsonFormat.Document 252 | > 253 | ): Promise>; 254 | 255 | scan< 256 | FilterExpression extends string | undefined = undefined, 257 | ProjectionExpression extends string | undefined = undefined, 258 | AttributesToGet extends keyof Item | undefined = undefined 259 | >( 260 | params: ScanInput< 261 | Item, 262 | FilterExpression, 263 | ProjectionExpression, 264 | AttributesToGet, 265 | JsonFormat.Document 266 | >, 267 | callback?: Callback< 268 | ScanOutput & MetadataBearer, 269 | any 270 | > 271 | ): void; 272 | } 273 | -------------------------------------------------------------------------------- /src/expression-attributes.ts: -------------------------------------------------------------------------------- 1 | import { AttributeValue, DocumentValue } from "./attribute-value"; 2 | import { JsonFormat } from "./json-format"; 3 | import { Word } from "./letter"; 4 | 5 | export type ExpressionAttributeValues< 6 | Expression extends string | undefined, 7 | Format extends JsonFormat 8 | > = undefined extends Expression 9 | ? {} 10 | : ParseConditionExpressionValues extends never 11 | ? {} 12 | : { 13 | ExpressionAttributeValues: { 14 | [name in ParseConditionExpressionValues as `:${name}`]: Format extends JsonFormat.AttributeValue 15 | ? AttributeValue 16 | : DocumentValue; 17 | }; 18 | }; 19 | 20 | export type ExpressionAttributeNames = 21 | undefined extends Expression 22 | ? {} 23 | : ParseConditionExpressionNames extends never 24 | ? {} 25 | : { 26 | ExpressionAttributeNames: { 27 | [name in ParseConditionExpressionNames as `#${name}`]: string; 28 | }; 29 | }; 30 | 31 | type ParseConditionExpressionNames = Extract< 32 | ParsePrefixedString<"#", Split>, 33 | string 34 | >; 35 | 36 | type ParseConditionExpressionValues = Extract< 37 | ParsePrefixedString<":", Split>, 38 | string 39 | >; 40 | 41 | // long expressions can easily reach the 50 depth limit 42 | // to work around this, Split will partition the string by the `,` delimiter. 43 | // the reason for `,` is because update expressions are separated by `,` 44 | // This means that we can support strings longer than 50. 45 | // The 50 max depth limit now only applies to the length of strings between commas, `,`. 46 | // @see https://github.com/sam-goodwin/typesafe-dynamodb/issues/29 47 | type Split = 48 | S extends `${infer pre},${infer post}` 49 | ? pre | Split 50 | : S extends undefined 51 | ? "" 52 | : S; 53 | 54 | type ParsePrefixedString< 55 | Prefix extends string, 56 | Str extends string | undefined = undefined, 57 | Names extends string | undefined = undefined 58 | > = undefined | "" extends Str 59 | ? Names 60 | : Str extends `${Prefix}${infer Tail}` 61 | ? // it is a name 62 | ParsePrefixedString< 63 | Prefix, 64 | Skip, 65 | undefined extends Names ? Read : Names | Read 66 | > 67 | : Str extends `${string}${infer Tail}` 68 | ? ParsePrefixedString 69 | : Names; 70 | 71 | type Skip< 72 | S extends string, 73 | Char extends string | number 74 | > = S extends `${Char}${infer Tail}` 75 | ? Skip 76 | : S extends `${Char}` 77 | ? "" 78 | : S; 79 | 80 | // type a = Read<"abc", "a" | "b">; 81 | 82 | type Read< 83 | S extends string, 84 | Char extends string | number, 85 | Accum extends string = "" 86 | > = S extends `${infer C}${infer rest}` 87 | ? C extends Char 88 | ? Read 89 | : Accum 90 | : Accum; 91 | -------------------------------------------------------------------------------- /src/expression.ts: -------------------------------------------------------------------------------- 1 | export type Expr = 2 | | ArrayIndex 3 | | Identifier 4 | | NumberLiteral 5 | | OperatorExpr 6 | | NameRef 7 | | PropRef 8 | | ValueRef; 9 | // 10 | 11 | export interface NameRef { 12 | kind: "name-ref"; 13 | name: Name; 14 | } 15 | export interface ValueRef { 16 | kind: "value-ref"; 17 | name: Name; 18 | } 19 | export interface Identifier { 20 | kind: "identifier"; 21 | name: I; 22 | } 23 | 24 | export interface NumberLiteral { 25 | kind: "index"; 26 | number: N; 27 | } 28 | export interface PropRef { 29 | kind: "prop-ref"; 30 | expr: Ex; 31 | name: Id; 32 | } 33 | export interface ArrayIndex< 34 | List extends Expr = any, 35 | Number extends NumberLiteral | ValueRef = NumberLiteral | ValueRef 36 | > { 37 | kind: "array-index"; 38 | list: List; 39 | number: Number; 40 | } 41 | 42 | export type Operator = "and" | "or" | "=" | "<" | "<=" | ">=" | ">"; 43 | export interface OperatorExpr< 44 | Left extends Expr = any, 45 | Op extends Operator = Operator, 46 | Right extends Expr = any 47 | > { 48 | kind: "op"; 49 | left: Left; 50 | op: Op; 51 | right: Right; 52 | } 53 | -------------------------------------------------------------------------------- /src/get-command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamoDBClientResolvedConfig, 3 | GetItemCommand as _GetItemCommand, 4 | } from "@aws-sdk/client-dynamodb"; 5 | import type { Command } from "@smithy/smithy-client"; 6 | import { TableKey } from "./key"; 7 | import { GetItemInput, GetItemOutput } from "./get-item"; 8 | import { MetadataBearer } from "@aws-sdk/types"; 9 | import { JsonFormat } from "./json-format"; 10 | import { Simplify } from "./simplify"; 11 | 12 | export type GetCommand< 13 | Item extends object, 14 | PartitionKey extends keyof Item, 15 | RangeKey extends keyof Item | undefined, 16 | Format extends JsonFormat 17 | > = new < 18 | Key extends TableKey, 19 | const AttributesToGet extends keyof Item | undefined, 20 | const ProjectionExpression extends string | undefined 21 | >( 22 | input: GetItemInput< 23 | Item, 24 | PartitionKey, 25 | RangeKey, 26 | Key, 27 | AttributesToGet, 28 | ProjectionExpression, 29 | Format 30 | > 31 | ) => Command< 32 | GetItemInput< 33 | Item, 34 | PartitionKey, 35 | RangeKey, 36 | Key, 37 | AttributesToGet, 38 | ProjectionExpression, 39 | Format 40 | >, 41 | Simplify< 42 | GetItemOutput< 43 | Item, 44 | PartitionKey, 45 | RangeKey, 46 | Key, 47 | AttributesToGet, 48 | ProjectionExpression, 49 | Format 50 | > & 51 | MetadataBearer 52 | >, 53 | DynamoDBClientResolvedConfig 54 | > & { 55 | _brand: "GetItemCommand"; 56 | }; 57 | -------------------------------------------------------------------------------- /src/get-document-command.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand as _GetCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { GetCommand } from "./get-command"; 4 | 5 | export function TypeSafeGetDocumentCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): GetCommand { 10 | return _GetCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/get-item-command.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand as _GetCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { GetCommand } from "./get-command"; 4 | 5 | export function TypeSafeGetItemCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): GetCommand { 10 | return _GetCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/get-item.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDB } from "aws-sdk"; 2 | import { JsonFormat, FormatObject } from "./json-format"; 3 | import { TableKey } from "./key"; 4 | import { Narrow } from "./narrow"; 5 | import { ApplyProjection } from "./projection"; 6 | import { Simplify } from "./simplify"; 7 | 8 | export interface GetItemInput< 9 | Item extends object, 10 | PartitionKey extends keyof Item, 11 | RangeKey extends keyof Item | undefined, 12 | Key extends TableKey, 13 | AttributesToGet extends keyof Item | undefined, 14 | ProjectionExpression extends string | undefined, 15 | Format extends JsonFormat 16 | > extends Omit< 17 | DynamoDB.GetItemInput, 18 | "Key" | "AttributesToGet" | "ProjectionExpression" 19 | > { 20 | Key: Key; 21 | AttributesToGet?: readonly AttributesToGet[] | AttributesToGet[]; 22 | ProjectionExpression?: ProjectionExpression; 23 | } 24 | 25 | export interface GetItemOutput< 26 | Item extends object, 27 | PartitionKey extends keyof Item, 28 | RangeKey extends keyof Item | undefined, 29 | Key extends TableKey, 30 | AttributesToGet extends keyof Item | undefined, 31 | ProjectionExpression extends string | undefined, 32 | Format extends JsonFormat = JsonFormat.AttributeValue 33 | > extends Omit { 34 | Item?: Simplify< 35 | FormatObject< 36 | undefined extends AttributesToGet 37 | ? undefined extends ProjectionExpression 38 | ? Narrow>, Format> 39 | : Extract< 40 | ApplyProjection< 41 | Narrow< 42 | Item, 43 | Extract>, 44 | Format 45 | >, 46 | Extract 47 | >, 48 | object 49 | > 50 | : Pick< 51 | Narrow< 52 | Item, 53 | Extract>, 54 | Format 55 | >, 56 | Extract 57 | >, 58 | Format 59 | > 60 | >; 61 | } 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { JsonFormat } from "./json-format"; 2 | -------------------------------------------------------------------------------- /src/json-format.ts: -------------------------------------------------------------------------------- 1 | import { ToAttributeMap } from "./attribute-value"; 2 | 3 | export enum JsonFormat { 4 | AttributeValue = "AttributeValue", 5 | Document = "Document", 6 | Default = AttributeValue, 7 | } 8 | 9 | export type FormatObject< 10 | T, 11 | Format extends JsonFormat 12 | > = Format extends JsonFormat.AttributeValue 13 | ? T extends undefined 14 | ? undefined 15 | : ToAttributeMap> 16 | : T; 17 | -------------------------------------------------------------------------------- /src/key.ts: -------------------------------------------------------------------------------- 1 | import { B, N, S } from "./attribute-value"; 2 | import { JsonFormat, FormatObject } from "./json-format"; 3 | 4 | export type TableKey< 5 | Item extends object, 6 | PartitionKey extends keyof Item, 7 | RangeKey extends keyof Item | undefined, 8 | Format extends JsonFormat 9 | > = FormatObject< 10 | Pick>, 11 | Format 12 | >; 13 | 14 | export type TableKeyAttributeToObject< 15 | Key extends TableKey 16 | > = { 17 | [attrName in keyof Key]: Key[attrName] extends S 18 | ? s 19 | : Key[attrName] extends N 20 | ? n 21 | : Key[attrName] extends B 22 | ? Buffer | string 23 | : never; 24 | }; 25 | -------------------------------------------------------------------------------- /src/letter.ts: -------------------------------------------------------------------------------- 1 | export type LowercaseLetter = 2 | | "a" 3 | | "b" 4 | | "c" 5 | | "d" 6 | | "e" 7 | | "f" 8 | | "g" 9 | | "h" 10 | | "i" 11 | | "j" 12 | | "k" 13 | | "l" 14 | | "m" 15 | | "n" 16 | | "o" 17 | | "p" 18 | | "q" 19 | | "r" 20 | | "s" 21 | | "t" 22 | | "u" 23 | | "v" 24 | | "w" 25 | | "x" 26 | | "y" 27 | | "z"; 28 | 29 | export type UppercaseLetter = Uppercase; 30 | 31 | export type Letter = LowercaseLetter | UppercaseLetter; 32 | 33 | export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; 34 | 35 | export type SpecialCharacter = "_"; 36 | 37 | export type Word = Digit | Letter | SpecialCharacter; 38 | -------------------------------------------------------------------------------- /src/marshall.ts: -------------------------------------------------------------------------------- 1 | import { 2 | marshallOptions, 3 | unmarshallOptions, 4 | marshall as _marshall, 5 | unmarshall as _unmarshall, 6 | } from "@aws-sdk/util-dynamodb"; 7 | import { 8 | AttributeMap, 9 | B, 10 | L, 11 | M, 12 | N, 13 | NativeBinaryAttribute, 14 | S, 15 | ToAttributeMap, 16 | } from "./attribute-value"; 17 | 18 | export const marshall: < 19 | Item extends object, 20 | MarshallOptions extends marshallOptions | undefined 21 | >( 22 | item: Item, 23 | options?: MarshallOptions 24 | ) => ToAttributeMap = _marshall; 25 | 26 | export const unmarshall: < 27 | Item extends AttributeMap, 28 | UnmarshallOptions extends unmarshallOptions | undefined 29 | >( 30 | item: Item, 31 | options?: UnmarshallOptions 32 | ) => { 33 | [prop in keyof Item]: Unmarshall; 34 | } = _unmarshall as any; 35 | 36 | export interface NumberValue { 37 | value: `${N}`; 38 | } 39 | 40 | export type Unmarshall< 41 | T, 42 | UnmarshallOptions extends unmarshallOptions | undefined 43 | > = T extends S 44 | ? s 45 | : T extends B 46 | ? NativeBinaryAttribute 47 | : T extends N 48 | ? Exclude["wrapNumbers"] extends true 49 | ? NumberValue 50 | : n 51 | : T extends Date 52 | ? string 53 | : T extends L 54 | ? { 55 | [i in keyof Items]: i extends "length" 56 | ? Items[i] 57 | : Unmarshall; 58 | } 59 | : T extends M 60 | ? { 61 | [prop in keyof Attributes]: Unmarshall< 62 | Attributes[prop], 63 | UnmarshallOptions 64 | >; 65 | } 66 | : never; 67 | -------------------------------------------------------------------------------- /src/narrow.ts: -------------------------------------------------------------------------------- 1 | import { JsonFormat } from "./json-format"; 2 | import { TableKey, TableKeyAttributeToObject } from "./key"; 3 | 4 | export type Narrow< 5 | Item extends object, 6 | Key extends TableKey, 7 | Format extends JsonFormat 8 | > = Extract< 9 | Item, 10 | Format extends JsonFormat.AttributeValue 11 | ? TableKeyAttributeToObject 12 | : Key 13 | >; 14 | -------------------------------------------------------------------------------- /src/projection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayIndex, 3 | Identifier, 4 | NameRef, 5 | NumberLiteral, 6 | Expr, 7 | PropRef, 8 | ValueRef, 9 | } from "./expression"; 10 | import { Word } from "./letter"; 11 | 12 | export type ParseProjectionExpression = Parse< 13 | Text, 14 | [], 15 | undefined 16 | >; 17 | 18 | type AsExpr = Extract; 19 | 20 | type Parse< 21 | Text extends string, 22 | Expressions extends Expr[], 23 | Exp extends Expr | undefined 24 | > = Text extends `.${infer Rest}` 25 | ? Exp extends Expr 26 | ? Parse>> 27 | : never 28 | : Text extends `[:${infer Rest}` 29 | ? Parse, ValueRef<"">>> 30 | : Text extends `[${infer Rest}` 31 | ? Parse, NumberLiteral<"">>> 32 | : Text extends `]${infer Rest}` 33 | ? Parse 34 | : Text extends `]` 35 | ? Concat 36 | : Text extends `${"," | " "}${infer Rest}` 37 | ? Parse, undefined> 38 | : Text extends `#${infer Rest}` 39 | ? Parse, NameRef> 40 | : Text extends `:${infer Rest}` 41 | ? Parse, ValueRef> 42 | : Text extends `${Word}${string}` 43 | ? Text extends `${infer char}${infer Rest}` 44 | ? Parse> 45 | : never 46 | : Text extends `${Word}${string}` 47 | ? Text extends `${infer char}${infer Rest}` 48 | ? Parse> 49 | : never 50 | : Concat; 51 | 52 | type Concat< 53 | Expressions extends Expr[], 54 | CurrentExpr extends Expr | undefined 55 | > = undefined extends CurrentExpr 56 | ? Expressions 57 | : [...Expressions, Extract]; 58 | 59 | type Append< 60 | Exp extends Expr | undefined, 61 | char extends string 62 | > = Exp extends undefined 63 | ? Identifier 64 | : Exp extends Identifier 65 | ? Identifier<`${Name}${char}`> 66 | : Exp extends NumberLiteral 67 | ? NumberLiteral<`${Name}${char}`> 68 | : Exp extends NameRef 69 | ? NameRef<`${Name}${char}`> 70 | : Exp extends ValueRef 71 | ? ValueRef<`${Name}${char}`> 72 | : Exp extends PropRef 73 | ? PropRef, Identifier>> 74 | : Exp extends ArrayIndex 75 | ? ArrayIndex, NumberLiteral>> 76 | : never; 77 | 78 | export type ApplyProjection = Flatten< 79 | UnionToIntersection< 80 | ApplyProjectionExpr[number]> 81 | > 82 | >; 83 | 84 | type ApplyProjectionExpr = T extends undefined 85 | ? never 86 | : Exp extends PropRef 87 | ? { 88 | [p in keyof ApplyProjectionExpr]: ApplyProjectionExpr< 89 | ApplyProjectionExpr[p], 90 | i 91 | >; 92 | } 93 | : Exp extends ArrayIndex 94 | ? { 95 | [p in keyof ApplyProjectionExpr]: ApplyProjectionExpr< 96 | ApplyProjectionExpr[keyof ApplyProjectionExpr], 97 | i 98 | >; 99 | } 100 | : Exp extends Identifier 101 | ? I extends keyof T 102 | ? Pick 103 | : never 104 | : Exp extends NumberLiteral 105 | ? ParseInt extends keyof T 106 | ? Pick> 107 | : never 108 | : never; 109 | 110 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( 111 | k: infer I 112 | ) => void 113 | ? I 114 | : never; 115 | 116 | type Flatten = T extends object 117 | ? keyof T extends number 118 | ? FlattenArray>> 119 | : { 120 | [k in keyof T]: Flatten; 121 | } 122 | : T; 123 | 124 | type FlattenArray< 125 | T extends { [i in number]: any }, 126 | i extends number = 0 127 | > = i extends keyof T 128 | ? [T[i], ...FlattenArray>] 129 | : number extends i 130 | ? [] 131 | : FlattenArray>; 132 | 133 | type Inc = N extends 0 134 | ? 1 135 | : N extends 1 136 | ? 2 137 | : N extends 2 138 | ? 3 139 | : N extends 3 140 | ? 4 141 | : N extends 4 142 | ? 5 143 | : N extends 5 144 | ? 6 145 | : N extends 6 146 | ? 7 147 | : number; 148 | 149 | type ParseInt = N extends "0" 150 | ? 0 151 | : N extends "1" 152 | ? 1 153 | : N extends "2" 154 | ? 2 155 | : N extends "3" 156 | ? 3 157 | : N extends "4" 158 | ? 4 159 | : N extends "5" 160 | ? 5 161 | : N extends "6" 162 | ? 6 163 | : N extends "7" 164 | ? 7 165 | : N extends "8" 166 | ? 8 167 | : N extends "9" 168 | ? 9 169 | : number; 170 | -------------------------------------------------------------------------------- /src/put-document-command.ts: -------------------------------------------------------------------------------- 1 | import { PutCommand as _PutCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { PutCommand } from "./put-item"; 4 | 5 | export function TypeSafePutDocumentCommand(): PutCommand< 6 | Item, 7 | JsonFormat.Document 8 | > { 9 | return _PutCommand as any; 10 | } 11 | -------------------------------------------------------------------------------- /src/put-item-command.ts: -------------------------------------------------------------------------------- 1 | import { PutItemCommand as _PutItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { PutCommand } from "./put-item"; 4 | 5 | export function TypeSafePutItemCommand(): PutCommand< 6 | Item, 7 | JsonFormat.AttributeValue 8 | > { 9 | return _PutItemCommand as any; 10 | } 11 | -------------------------------------------------------------------------------- /src/put-item.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDB } from "aws-sdk"; 2 | import type { 3 | ExpressionAttributeNames, 4 | ExpressionAttributeValues, 5 | } from "./expression-attributes"; 6 | import type { FormatObject, JsonFormat } from "./json-format"; 7 | import type { 8 | DynamoDBClientResolvedConfig, 9 | ReturnValue as DynamoDBReturnValue, 10 | } from "@aws-sdk/client-dynamodb"; 11 | import type { Command } from "@smithy/smithy-client"; 12 | import type { MetadataBearer } from "@aws-sdk/types"; 13 | 14 | export type PutItemInput< 15 | Item extends object, 16 | ConditionExpression extends string | undefined, 17 | ReturnValue extends DynamoDB.ReturnValue, 18 | Format extends JsonFormat 19 | > = Omit< 20 | DynamoDB.PutItemInput, 21 | | "ConditionExpression" 22 | | "ExpressionAttributeNames" 23 | | "ExpressionAttributeValues" 24 | | "Item" 25 | | "ReturnValues" 26 | > & 27 | ExpressionAttributeNames & 28 | ExpressionAttributeValues & { 29 | Item: FormatObject; 30 | ReturnValues?: ReturnValue; 31 | ConditionExpression?: ConditionExpression; 32 | }; 33 | 34 | export interface PutItemOutput< 35 | Item extends object, 36 | ReturnValue extends DynamoDB.ReturnValue, 37 | Format extends JsonFormat 38 | > extends Omit { 39 | Attributes?: "ALL_OLD" | "ALL_NEW" extends ReturnValue 40 | ? FormatObject 41 | : undefined | "NONE" extends ReturnValue 42 | ? undefined 43 | : "UPDATED_OLD" | "UPDATED_NEW" extends ReturnValue 44 | ? Partial> 45 | : Partial>; 46 | } 47 | 48 | export type PutCommand = new < 49 | const ConditionExpression extends string | undefined = undefined, 50 | const ReturnValue extends DynamoDBReturnValue = "NONE" 51 | >( 52 | input: PutItemInput 53 | ) => Command< 54 | PutItemInput, 55 | PutItemOutput & MetadataBearer, 56 | DynamoDBClientResolvedConfig 57 | > & { 58 | _brand: "PutItemCommand"; 59 | }; 60 | -------------------------------------------------------------------------------- /src/query-command.ts: -------------------------------------------------------------------------------- 1 | import { QueryCommand as _QueryCommand } from "@aws-sdk/client-dynamodb"; 2 | import { QueryCommand } from "./query"; 3 | import { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeQueryCommand(): QueryCommand< 6 | Item, 7 | JsonFormat.AttributeValue 8 | > { 9 | return _QueryCommand as any; 10 | } 11 | -------------------------------------------------------------------------------- /src/query-document-command.ts: -------------------------------------------------------------------------------- 1 | import { QueryCommand as _QueryCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { QueryCommand } from "./query"; 4 | 5 | export function TypeSafeQueryDocumentCommand< 6 | Item extends object 7 | >(): QueryCommand { 8 | return _QueryCommand as any; 9 | } 10 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDBClientResolvedConfig } from "@aws-sdk/client-dynamodb"; 2 | import type { Command } from "@smithy/smithy-client"; 3 | import type { MetadataBearer } from "@aws-sdk/types"; 4 | import type { DynamoDB } from "aws-sdk"; 5 | import type { 6 | ExpressionAttributeNames, 7 | ExpressionAttributeValues, 8 | } from "./expression-attributes"; 9 | import type { FormatObject, JsonFormat } from "./json-format"; 10 | 11 | export type QueryInput< 12 | Item extends object, 13 | KeyConditionExpression extends string | undefined, 14 | FilterExpression extends string | undefined, 15 | ProjectionExpression extends string | undefined, 16 | AttributesToGet extends keyof Item | undefined, 17 | Format extends JsonFormat 18 | > = Omit< 19 | DynamoDB.QueryInput, 20 | | "AttributesToGet" 21 | | "KeyConditionExpression" 22 | | "FilterExpression" 23 | | "ExpressionAttributeNames" 24 | | "ExpressionAttributeValues" 25 | > & 26 | ExpressionAttributeNames & 27 | ExpressionAttributeNames & 28 | ExpressionAttributeNames & 29 | ExpressionAttributeValues & 30 | ExpressionAttributeValues & { 31 | KeyConditionExpression?: KeyConditionExpression; 32 | FilterExpression?: FilterExpression; 33 | ProjectionExpression?: ProjectionExpression; 34 | AttributesToGet?: AttributesToGet[]; 35 | }; 36 | 37 | export interface QueryOutput< 38 | Item extends object, 39 | AttributesToGet extends keyof Item | undefined, 40 | Format extends JsonFormat 41 | > extends Omit { 42 | Items?: FormatObject< 43 | undefined extends AttributesToGet 44 | ? Item 45 | : Pick>, 46 | Format 47 | >[]; 48 | } 49 | 50 | export type QueryCommand = new < 51 | const KeyConditionExpression extends string | undefined, 52 | const FilterExpression extends string | undefined, 53 | const ProjectionExpression extends string | undefined, 54 | const AttributesToGet extends keyof Item | undefined 55 | >( 56 | input: QueryInput< 57 | Item, 58 | KeyConditionExpression, 59 | FilterExpression, 60 | ProjectionExpression, 61 | AttributesToGet, 62 | Format 63 | > 64 | ) => Command< 65 | QueryInput< 66 | Item, 67 | KeyConditionExpression, 68 | FilterExpression, 69 | ProjectionExpression, 70 | AttributesToGet, 71 | Format 72 | >, 73 | QueryOutput & MetadataBearer, 74 | DynamoDBClientResolvedConfig 75 | > & { 76 | _brand: "QueryCommand"; 77 | }; 78 | -------------------------------------------------------------------------------- /src/scan-command.ts: -------------------------------------------------------------------------------- 1 | import { ScanCommand as _ScanCommand } from "@aws-sdk/client-dynamodb"; 2 | import type { ScanCommand } from "./scan"; 3 | import type { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeScanCommand(): ScanCommand< 6 | Item, 7 | JsonFormat.AttributeValue 8 | > { 9 | return _ScanCommand as any; 10 | } 11 | -------------------------------------------------------------------------------- /src/scan-document-command.ts: -------------------------------------------------------------------------------- 1 | import { ScanCommand as _ScanCommand } from "@aws-sdk/client-dynamodb"; 2 | import type { JsonFormat } from "./json-format"; 3 | import type { ScanCommand } from "./scan"; 4 | 5 | export function TypeSafeScanDocumentCommand(): ScanCommand< 6 | Item, 7 | JsonFormat.Document 8 | > { 9 | return _ScanCommand as any; 10 | } 11 | -------------------------------------------------------------------------------- /src/scan.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDB } from "aws-sdk"; 2 | import type { 3 | ExpressionAttributeNames, 4 | ExpressionAttributeValues, 5 | } from "./expression-attributes"; 6 | import type { FormatObject, JsonFormat } from "./json-format"; 7 | import type { 8 | DynamoDBClientResolvedConfig, 9 | ScanCommand as _ScanCommand, 10 | } from "@aws-sdk/client-dynamodb"; 11 | import type { Command } from "@smithy/smithy-client"; 12 | import type { MetadataBearer } from "@aws-sdk/types"; 13 | 14 | export type ScanInput< 15 | Item extends object, 16 | FilterExpression extends string | undefined, 17 | ProjectionExpression extends string | undefined, 18 | AttributesToGet extends keyof Item | undefined, 19 | Format extends JsonFormat 20 | > = Omit< 21 | DynamoDB.ScanInput, 22 | | "AttributesToGet" 23 | | "FilterExpression" 24 | | "ExpressionAttributeNames" 25 | | "ExpressionAttributeValues" 26 | > & 27 | ExpressionAttributeNames & 28 | ExpressionAttributeValues & { 29 | FilterExpression?: FilterExpression; 30 | ProjectionExpression?: ProjectionExpression; 31 | AttributesToGet?: AttributesToGet[]; 32 | }; 33 | 34 | export interface ScanOutput< 35 | Item extends object, 36 | AttributesToGet extends keyof Item | undefined, 37 | Format extends JsonFormat 38 | > extends Omit { 39 | Items?: FormatObject< 40 | undefined extends AttributesToGet 41 | ? Item 42 | : Pick>, 43 | Format 44 | >[]; 45 | } 46 | 47 | export type ScanCommand = new < 48 | const FilterExpression extends string | undefined, 49 | const ProjectionExpression extends string | undefined, 50 | const AttributesToGet extends keyof Item | undefined 51 | >( 52 | input: ScanInput< 53 | Item, 54 | FilterExpression, 55 | ProjectionExpression, 56 | AttributesToGet, 57 | Format 58 | > 59 | ) => Command< 60 | ScanInput< 61 | Item, 62 | FilterExpression, 63 | ProjectionExpression, 64 | AttributesToGet, 65 | Format 66 | >, 67 | ScanOutput & MetadataBearer, 68 | DynamoDBClientResolvedConfig 69 | > & { 70 | _brand: "ScanCommand"; 71 | }; 72 | -------------------------------------------------------------------------------- /src/simplify.ts: -------------------------------------------------------------------------------- 1 | export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; 2 | -------------------------------------------------------------------------------- /src/stream-event.ts: -------------------------------------------------------------------------------- 1 | import * as lambda from "aws-lambda"; 2 | import { ToAttributeMap } from "./attribute-value"; 3 | 4 | export interface DynamoDBStreamEvent< 5 | Item extends object, 6 | PartitionKey extends keyof Item, 7 | RangeKey extends keyof Item | undefined, 8 | StreamViewType extends Exclude< 9 | lambda.StreamRecord["StreamViewType"], 10 | undefined 11 | > 12 | > { 13 | Records: DynamoDBRecord[]; 14 | } 15 | export interface DynamoDBRecord< 16 | Item extends object, 17 | PartitionKey extends keyof Item, 18 | RangeKey extends keyof Item | undefined, 19 | StreamViewType extends lambda.StreamRecord["StreamViewType"] = undefined 20 | > extends Omit { 21 | dynamodb?: StreamRecord; 22 | } 23 | 24 | // @ts-ignore 25 | export type StreamRecord< 26 | Item extends object, 27 | PartitionKey extends keyof Item, 28 | RangeKey extends keyof Item | undefined, 29 | StreamViewType extends lambda.StreamRecord["StreamViewType"] = undefined 30 | > = Omit< 31 | lambda.StreamRecord, 32 | "Keys" | "NewImage" | "OldImage" | "StreamViewType" 33 | > & { 34 | Keys: ToAttributeMap>>; 35 | } & (StreamViewType extends "NEW_IMAGE" 36 | ? { 37 | NewImage: ToAttributeMap; 38 | } 39 | : StreamViewType extends "OLD_IMAGE" 40 | ? { 41 | OldImage: ToAttributeMap; 42 | } 43 | : StreamViewType extends "NEW_AND_OLD_IMAGES" 44 | ? { 45 | NewImage?: ToAttributeMap; 46 | OldImage: ToAttributeMap; 47 | } 48 | : {}); 49 | -------------------------------------------------------------------------------- /src/update-document-command.ts: -------------------------------------------------------------------------------- 1 | import { UpdateCommand as _UpdateCommand } from "@aws-sdk/lib-dynamodb"; 2 | import type { UpdateCommand } from "./update-item"; 3 | import type { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeUpdateDocumentCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): UpdateCommand { 10 | return _UpdateCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/update-item-command.ts: -------------------------------------------------------------------------------- 1 | import { UpdateItemCommand as _UpdateItemCommand } from "@aws-sdk/client-dynamodb"; 2 | import { UpdateCommand } from "./update-item"; 3 | import { JsonFormat } from "./json-format"; 4 | 5 | export function TypeSafeUpdateItemCommand< 6 | Item extends object, 7 | PartitionKey extends keyof Item, 8 | RangeKey extends keyof Item | undefined 9 | >(): UpdateCommand { 10 | return _UpdateItemCommand as any; 11 | } 12 | -------------------------------------------------------------------------------- /src/update-item.ts: -------------------------------------------------------------------------------- 1 | import type { DynamoDB } from "aws-sdk"; 2 | import type { 3 | ExpressionAttributeNames, 4 | ExpressionAttributeValues, 5 | } from "./expression-attributes"; 6 | import type { FormatObject, JsonFormat } from "./json-format"; 7 | import type { TableKey } from "./key"; 8 | import type { Narrow } from "./narrow"; 9 | import type { 10 | DynamoDBClientResolvedConfig, 11 | ReturnValue as DynamoDBReturnValue, 12 | } from "@aws-sdk/client-dynamodb"; 13 | import type { MetadataBearer } from "@aws-sdk/types"; 14 | import type { Command } from "@smithy/smithy-client"; 15 | 16 | export type UpdateItemInput< 17 | Item extends object, 18 | PartitionKey extends keyof Item, 19 | RangeKey extends keyof Item | undefined, 20 | Key extends TableKey, 21 | UpdateExpression extends string, 22 | ConditionExpression extends string | undefined, 23 | ReturnValue extends DynamoDB.ReturnValue, 24 | Format extends JsonFormat 25 | > = Omit< 26 | DynamoDB.UpdateItemInput, 27 | | "ConditionExpression" 28 | | "UpdateExpression" 29 | | "ExpressionAttributeNames" 30 | | "ExpressionAttributeValues" 31 | | "Key" 32 | | "ReturnValues" 33 | > & 34 | ExpressionAttributeNames & 35 | ExpressionAttributeValues & 36 | ExpressionAttributeNames & 37 | ExpressionAttributeValues & { 38 | Key: Key; 39 | ReturnValues?: ReturnValue; 40 | UpdateExpression: UpdateExpression; 41 | ConditionExpression?: ConditionExpression; 42 | }; 43 | 44 | export interface UpdateItemOutput< 45 | Item extends object, 46 | PartitionKey extends keyof Item, 47 | RangeKey extends keyof Item | undefined, 48 | Key extends TableKey, 49 | ReturnValue extends DynamoDB.ReturnValue, 50 | Format extends JsonFormat 51 | > extends Omit { 52 | Attributes?: FormatObject< 53 | ReturnValue extends undefined | "NONE" 54 | ? undefined 55 | : ReturnValue extends "ALL_OLD" | "ALL_NEW" 56 | ? Narrow>, Format> 57 | : ReturnValue extends "UPDATED_OLD" | "UPDATED_NEW" 58 | ? Partial< 59 | Narrow>, Format> 60 | > 61 | : Partial< 62 | Narrow>, Format> 63 | >, 64 | Format 65 | >; 66 | } 67 | 68 | export type UpdateCommand< 69 | Item extends object, 70 | PartitionKey extends keyof Item, 71 | RangeKey extends keyof Item | undefined, 72 | Format extends JsonFormat 73 | > = new < 74 | Key extends TableKey, 75 | const UpdateExpression extends string, 76 | const ConditionExpression extends string | undefined = undefined, 77 | const ReturnValue extends DynamoDBReturnValue = "NONE" 78 | >( 79 | input: UpdateItemInput< 80 | Item, 81 | PartitionKey, 82 | RangeKey, 83 | Key, 84 | UpdateExpression, 85 | ConditionExpression, 86 | ReturnValue, 87 | Format 88 | > 89 | ) => Command< 90 | UpdateItemInput< 91 | Item, 92 | PartitionKey, 93 | RangeKey, 94 | Key, 95 | UpdateExpression, 96 | ConditionExpression, 97 | ReturnValue, 98 | Format 99 | >, 100 | UpdateItemOutput & 101 | MetadataBearer, 102 | DynamoDBClientResolvedConfig 103 | > & { 104 | _brand: "UpdateItemCommand"; 105 | }; 106 | -------------------------------------------------------------------------------- /test/expression-attributes.test.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | import * as AWS from "aws-sdk"; 4 | import { TypeSafeDocumentClientV2 } from "../src/document-client-v2"; 5 | 6 | export interface Order< 7 | UserID extends string = string, 8 | OrderID extends string = string 9 | > { 10 | PK: `USER#${UserID}`; 11 | SK: `ORDER#${OrderID}`; 12 | userId: string; 13 | orderId: string; 14 | } 15 | 16 | test("long UpdateExpression should compile", () => { 17 | const client: TypeSafeDocumentClientV2 = 18 | new AWS.DynamoDB.DocumentClient() as any; 19 | 20 | const userId = "userId"; 21 | const orderId = "orderId"; 22 | 23 | () => { 24 | client 25 | .update({ 26 | TableName: "", 27 | Key: { 28 | PK: `USER#${userId}`, 29 | SK: `ORDER#${orderId}`, 30 | }, 31 | UpdateExpression: `SET MyMapName.myLongFieldNameA = :myLongFieldValueA, 32 | MyMapName.myLongFieldNameB = :myLongFieldValueB, 33 | MyMapName.myLongFieldNameC = :myLongFieldValueC, 34 | MyMapName.myLongFieldNameD = :myLongFieldValueD, 35 | MyMapName.myLongFieldNameE = :myLongFieldValueE, 36 | MyMapName.myLongFieldNameF = :myLongFieldValueF, 37 | MyMapName.myLongFieldNameG = :myLongFieldValueG, 38 | MyMapName.myLongFieldNameG = :myLongFieldValueH, 39 | MyMapName.myLongFieldNameG = :myLongFieldValueI, 40 | MyMapName.myLongFieldNameG = :myLongFieldValueJ, 41 | MyMapName.myLongFieldNameG = :myLongFieldValueK, 42 | MyMapName.myLongFieldNameG = :myLongFieldValueL, 43 | MyMapName.myLongFieldNameG = :myLongFieldValueM, 44 | MyMapName.myLongFieldNameG = :myLongFieldValueN, 45 | `, 46 | ExpressionAttributeValues: { 47 | ":myLongFieldValueA": 1, 48 | ":myLongFieldValueB": 1, 49 | ":myLongFieldValueC": 1, 50 | ":myLongFieldValueD": 1, 51 | ":myLongFieldValueE": 1, 52 | ":myLongFieldValueF": 1, 53 | ":myLongFieldValueG": 1, 54 | ":myLongFieldValueH": 1, 55 | ":myLongFieldValueI": 1, 56 | ":myLongFieldValueJ": 1, 57 | ":myLongFieldValueK": 1, 58 | ":myLongFieldValueL": 1, 59 | ":myLongFieldValueM": 1, 60 | ":myLongFieldValueN": 1, 61 | }, 62 | ReturnValues: "NONE", 63 | }) 64 | .promise(); 65 | }; 66 | }); 67 | -------------------------------------------------------------------------------- /test/marshall.test.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | import { marshall, unmarshall } from "../src/marshall"; 4 | 5 | const myObject = { 6 | key: "key", 7 | sort: 123, 8 | binary: new Uint16Array([1]), 9 | buffer: Buffer.from("buffer", "utf8"), 10 | optional: 456, 11 | list: ["hello", "world"], 12 | record: { 13 | key: "nested key", 14 | sort: 789, 15 | }, 16 | } as const; 17 | 18 | test("should marshall MyItem to ToAttributeMap", () => { 19 | const marshalled = marshall(myObject); 20 | 21 | marshalled.key.S; 22 | marshalled.sort.N; 23 | marshalled.binary?.B; 24 | marshalled.buffer?.B; 25 | marshalled.optional?.N; 26 | marshalled.list?.L[0].S; 27 | marshalled.list?.L[1].S; 28 | // @ts-expect-error 29 | marshalled.list?.L[2]?.S; 30 | marshalled.record.M.key.S; 31 | marshalled.record.M.sort.N; 32 | }); 33 | 34 | test("should unmarshall MyItem from ToAttributeMap", () => { 35 | const marshalled = marshall(myObject); 36 | const unmarshalled = unmarshall(marshalled); 37 | 38 | expect(unmarshalled).toEqual(myObject); 39 | 40 | unmarshalled.key; 41 | unmarshalled.sort; 42 | unmarshalled.binary; 43 | unmarshalled.buffer; 44 | unmarshalled.optional.toString(10); // is a number 45 | unmarshalled.list?.[0]; 46 | unmarshalled.list?.[1]; 47 | // @ts-expect-error 48 | unmarshalled.list?.[2]; 49 | unmarshalled.record.key; 50 | unmarshalled.record.sort.toString(10); // is a number 51 | }); 52 | 53 | test("unmarshall should map numbers to string when wrapNumbers: true", () => { 54 | const marshalled = marshall(myObject); 55 | const unmarshalled = unmarshall(marshalled, { 56 | wrapNumbers: true, 57 | }); 58 | 59 | const expected: typeof unmarshalled = { 60 | ...myObject, 61 | sort: { 62 | value: "123", 63 | }, 64 | optional: { 65 | value: "456", 66 | }, 67 | record: { 68 | key: "nested key", 69 | sort: { 70 | value: "789", 71 | }, 72 | }, 73 | }; 74 | expect(unmarshalled).toEqual(expected); 75 | 76 | unmarshalled.key; 77 | unmarshalled.sort.value; // wrapped NumberValue 78 | unmarshalled.binary; 79 | unmarshalled.buffer; 80 | unmarshalled.optional?.value; // wrapped NumberValue 81 | unmarshalled.list?.[0]; 82 | unmarshalled.list?.[1]; 83 | // @ts-expect-error 84 | unmarshalled.list?.[2]; 85 | unmarshalled.record.key; 86 | unmarshalled.record.sort; 87 | }); 88 | -------------------------------------------------------------------------------- /test/stream-event.test.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBStreamEvent } from "../src/stream-event"; 2 | 3 | interface MyType { 4 | id: string; 5 | sort: number; 6 | children?: MyType[]; 7 | } 8 | 9 | test("dummy", () => { 10 | expect(1).toBe(1); 11 | }); 12 | 13 | export function handle_keys_only( 14 | event: DynamoDBStreamEvent 15 | ) { 16 | for (const record of event.Records) { 17 | record.dynamodb?.Keys; 18 | // @ts-expect-error 19 | record.dynamodb?.NewImage; 20 | // @ts-expect-error 21 | record.dynamodb?.OldImage; 22 | } 23 | } 24 | 25 | export function handle_new_image( 26 | event: DynamoDBStreamEvent 27 | ) { 28 | for (const record of event.Records) { 29 | record.dynamodb?.Keys; 30 | record.dynamodb?.NewImage; 31 | // @ts-expect-error 32 | record.dynamodb?.OldImage; 33 | } 34 | } 35 | 36 | export function handle_old_image( 37 | event: DynamoDBStreamEvent 38 | ) { 39 | for (const record of event.Records) { 40 | record.dynamodb?.Keys; 41 | // @ts-expect-error 42 | record.dynamodb?.NewImage; 43 | record.dynamodb?.OldImage; 44 | } 45 | } 46 | 47 | export function handle_new_and_old_images( 48 | event: DynamoDBStreamEvent 49 | ) { 50 | for (const record of event.Records) { 51 | record.dynamodb?.Keys; 52 | record.dynamodb?.NewImage; 53 | record.dynamodb?.OldImage; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.dev.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/v2-document.test.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | test("dummy", () => { 4 | expect(1).toBe(1); 5 | }); 6 | 7 | import { DynamoDB } from "aws-sdk"; 8 | 9 | import { ApplyProjection } from "../src/projection"; 10 | import { TypeSafeDocumentClientV2 } from "../src/document-client-v2"; 11 | 12 | export interface MyItem { 13 | pk: string; 14 | sk: string; 15 | attr: string; 16 | list?: MyItem[]; 17 | } 18 | 19 | const table = new DynamoDB.DocumentClient() as TypeSafeDocumentClientV2< 20 | MyItem, 21 | "pk", 22 | "sk" 23 | >; 24 | 25 | export async function foo(userId: string) { 26 | const response = await table 27 | .query({ 28 | TableName: "TableName", 29 | AttributesToGet: ["attr"], 30 | KeyConditionExpression: "#key = :val", 31 | FilterExpression: "#key2 = :val2", 32 | ExpressionAttributeNames: { 33 | "#key": "", 34 | "#key2": "", 35 | }, 36 | ExpressionAttributeValues: { 37 | ":val": "val", 38 | ":val2": true, 39 | }, 40 | }) 41 | .promise(); 42 | 43 | // using only projection 44 | await table 45 | .query({ 46 | TableName: "TableName", 47 | AttributesToGet: ["attr"], 48 | KeyConditionExpression: "key = :val", 49 | FilterExpression: "key2 = :val2", 50 | ProjectionExpression: "#key1", 51 | ExpressionAttributeNames: { 52 | "#key1": "", 53 | }, 54 | ExpressionAttributeValues: { 55 | ":val": "val", 56 | ":val2": true, 57 | }, 58 | }) 59 | .promise(); 60 | 61 | if (response.Items) { 62 | response.Items[0].attr; 63 | } 64 | 65 | await table 66 | .get({ 67 | TableName: "", 68 | Key: { 69 | pk: `USER#${userId}`, 70 | sk: "", 71 | }, 72 | ProjectionExpression: "attr", 73 | }) 74 | .promise(); 75 | 76 | await table 77 | .delete({ 78 | TableName: "", 79 | Key: { 80 | pk: `USER#${userId}`, 81 | sk: "", 82 | }, 83 | }) 84 | .promise(); 85 | 86 | await table 87 | .put({ 88 | TableName: "", 89 | Item: { 90 | pk: "pk", 91 | sk: "sk", 92 | attr: "attr", 93 | list: [ 94 | { 95 | pk: "pk", 96 | sk: "sk", 97 | attr: "attr", 98 | }, 99 | ], 100 | }, 101 | ConditionExpression: "#name = :value and #n = 0", 102 | ExpressionAttributeValues: { 103 | ":value": { 104 | N: "1", 105 | }, 106 | }, 107 | ExpressionAttributeNames: { 108 | "#name": "sam", 109 | "#n": "sam", 110 | }, 111 | }) 112 | .promise(); 113 | 114 | interface Record { 115 | id: string; 116 | record?: Record; 117 | items: (Record | undefined)[]; 118 | tuple: [a: "string", b: number]; 119 | } 120 | 121 | type A1 = ApplyProjection; 122 | const a1: A1 = null as any; 123 | a1.id; 124 | type A2 = ApplyProjection; 125 | const a2: A2 = null as any; 126 | a2.record?.id; 127 | type A3 = ApplyProjection; 128 | const a3: A3 = null as any; 129 | a3.record?.id; 130 | 131 | type A4 = ApplyProjection; 132 | const a4: A4 = null as any; 133 | a4.items[0]; 134 | 135 | type A5 = ApplyProjection; 136 | const a5: A5 = null as any; 137 | a5.tuple[0]; 138 | a5.tuple[1]; 139 | 140 | type A6 = ApplyProjection; 141 | const a6: A6 = null as any; 142 | a6.tuple[0]; 143 | a6.tuple[1]; 144 | } 145 | 146 | const TableName = "test"; 147 | 148 | interface User { 149 | PK: `USER#${UserID}`; 150 | SK: `#PROFILE#${UserID}`; 151 | Username: string; 152 | FullName: string; 153 | Email: string; 154 | CreatedAt: Date; 155 | Address: string; 156 | } 157 | 158 | interface Order< 159 | UserID extends string = string, 160 | OrderID extends string = string 161 | > { 162 | PK: `USER#${UserID}`; 163 | SK: `ORDER#${OrderID}`; 164 | Username: string; 165 | /** 166 | * Order ID. 167 | */ 168 | OrderID: OrderID; 169 | Status: "PLACED" | "SHIPPED"; 170 | CreatedAt: Date; 171 | Address: string; 172 | } 173 | 174 | declare const client: TypeSafeDocumentClientV2; 175 | 176 | export async function getProfile(userId: String) { 177 | const profile = await client 178 | .get({ 179 | TableName, 180 | Key: { 181 | PK: `USER#${userId}`, 182 | SK: `#PROFILE#${userId}`, 183 | }, 184 | }) 185 | .promise(); 186 | 187 | profile.Item?.FullName; 188 | // @ts-expect-error 189 | profile.Item?.OrderID; 190 | } 191 | 192 | export async function getOrder(userId: string, orderId: string) { 193 | const order = await client 194 | .get({ 195 | TableName, 196 | Key: { 197 | PK: `USER#${userId}`, 198 | SK: `ORDER#${orderId}`, 199 | }, 200 | }) 201 | .promise(); 202 | 203 | order.Item?.OrderID; 204 | // @ts-expect-error 205 | order.Item?.FullName; 206 | } 207 | 208 | export async function updateOrder(userId: string, orderId: string) { 209 | const defaultBehavior = await client 210 | .update({ 211 | TableName: "", 212 | Key: { 213 | PK: `USER#${userId}`, 214 | SK: `ORDER#${orderId}`, 215 | }, 216 | UpdateExpression: "#k = :v", 217 | ExpressionAttributeNames: { 218 | "#k": "list", 219 | }, 220 | ExpressionAttributeValues: { 221 | ":v": "val", 222 | ":v2": "val2", 223 | }, 224 | ConditionExpression: "#k = :v2", 225 | }) 226 | .promise(); 227 | // @ts-expect-error - default ReturnValues is None 228 | defaultBehavior.Attributes?.defaultBehavior.Attributes?.key; 229 | 230 | const returnNone = await client 231 | .update({ 232 | TableName: "", 233 | Key: { 234 | PK: `USER#${userId}`, 235 | SK: `ORDER#${orderId}`, 236 | }, 237 | UpdateExpression: "a = 1", 238 | ReturnValues: "NONE", 239 | }) 240 | .promise(); 241 | // @ts-expect-error - nothing is Returned 242 | returnNone.Attributes.PK.S; 243 | 244 | const returnAllNew = await client 245 | .update({ 246 | TableName: "", 247 | Key: { 248 | PK: `USER#${userId}`, 249 | SK: `ORDER#${orderId}`, 250 | }, 251 | UpdateExpression: "a = 1", 252 | ReturnValues: "ALL_NEW", 253 | }) 254 | .promise(); 255 | returnAllNew.Attributes?.PK; 256 | returnAllNew.Attributes?.OrderID; 257 | // @ts-expect-error 258 | returnAllNew.Attributes?.FullName; 259 | 260 | const returnAllOld = await client 261 | .update({ 262 | TableName: "", 263 | Key: { 264 | PK: `USER#${userId}`, 265 | SK: `ORDER#${orderId}`, 266 | }, 267 | UpdateExpression: "a = 1", 268 | ReturnValues: "ALL_OLD", 269 | }) 270 | .promise(); 271 | returnAllOld.Attributes?.PK?.length; 272 | 273 | const returnUpdatedNew = await client 274 | .update({ 275 | TableName: "", 276 | Key: { 277 | PK: `USER#${userId}`, 278 | SK: `ORDER#${orderId}`, 279 | }, 280 | UpdateExpression: "a = 1", 281 | ReturnValues: "UPDATED_NEW", 282 | }) 283 | .promise(); 284 | returnUpdatedNew.Attributes?.PK?.length; 285 | 286 | const returnUpdatedOld = await client 287 | .update({ 288 | TableName: "", 289 | Key: { 290 | PK: `USER#${userId}`, 291 | SK: `ORDER#${orderId}`, 292 | }, 293 | UpdateExpression: "a = 1", 294 | ReturnValues: "UPDATED_OLD", 295 | }) 296 | .promise(); 297 | returnUpdatedOld.Attributes?.PK?.length; 298 | 299 | const narrowedType = await client 300 | .update({ 301 | TableName: "", 302 | Key: { 303 | PK: `USER#${userId}`, 304 | 305 | SK: `ORDER#${orderId}`, 306 | }, 307 | UpdateExpression: "a = 1", 308 | ReturnValues: "UPDATED_OLD", 309 | }) 310 | .promise(); 311 | 312 | narrowedType.Attributes?.OrderID?.length; 313 | // @ts-expect-error - FullName does not exist on order 314 | narrowedType.Attributes?.FullName?.length; 315 | } 316 | -------------------------------------------------------------------------------- /test/v2.test.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | test("dummy", () => { 4 | expect(1).toBe(1); 5 | }); 6 | 7 | import { DynamoDB } from "aws-sdk"; 8 | 9 | import { TypeSafeDynamoDBv2 } from "../src/client-v2"; 10 | import { ApplyProjection } from "../src/projection"; 11 | 12 | export interface MyItem { 13 | pk: string; 14 | sk: string; 15 | attr: string; 16 | list?: MyItem[]; 17 | } 18 | 19 | const table = new DynamoDB() as unknown as TypeSafeDynamoDBv2< 20 | MyItem, 21 | "pk", 22 | "sk" 23 | >; 24 | 25 | export async function foo(userId: string) { 26 | const response = await table 27 | .query({ 28 | TableName: "TableName", 29 | AttributesToGet: ["attr"], 30 | KeyConditionExpression: "#key = :val", 31 | FilterExpression: "#key2 = :val2", 32 | ExpressionAttributeNames: { 33 | "#key": "", 34 | "#key2": "", 35 | }, 36 | ExpressionAttributeValues: { 37 | ":val": { 38 | S: "val", 39 | }, 40 | ":val2": { 41 | BOOL: true, 42 | }, 43 | }, 44 | }) 45 | .promise(); 46 | 47 | if (response.Items) { 48 | response.Items[0].attr; 49 | } 50 | 51 | await table 52 | .getItem({ 53 | TableName: "", 54 | Key: { 55 | pk: { 56 | S: `USER#${userId}`, 57 | }, 58 | sk: { 59 | S: "", 60 | }, 61 | }, 62 | ProjectionExpression: "attr", 63 | }) 64 | .promise(); 65 | 66 | await table 67 | .deleteItem({ 68 | TableName: "", 69 | Key: { 70 | pk: { 71 | S: `USER#${userId}`, 72 | }, 73 | sk: { 74 | S: "", 75 | }, 76 | }, 77 | }) 78 | .promise(); 79 | 80 | await table 81 | .putItem({ 82 | TableName: "", 83 | Item: { 84 | pk: { 85 | S: "pk", 86 | }, 87 | sk: { 88 | S: "sk", 89 | }, 90 | attr: { 91 | S: "attr", 92 | }, 93 | list: { 94 | L: [ 95 | { 96 | M: { 97 | pk: { 98 | S: "pk", 99 | }, 100 | sk: { 101 | S: "sk", 102 | }, 103 | attr: { 104 | S: "attr", 105 | }, 106 | }, 107 | }, 108 | ], 109 | }, 110 | }, 111 | ConditionExpression: "#name = :value and #n = 0", 112 | ExpressionAttributeValues: { 113 | ":value": { 114 | N: "1", 115 | }, 116 | }, 117 | ExpressionAttributeNames: { 118 | "#name": "sam", 119 | "#n": "sam", 120 | }, 121 | }) 122 | .promise(); 123 | 124 | interface Record { 125 | id: string; 126 | record?: Record; 127 | items: (Record | undefined)[]; 128 | tuple: [a: "string", b: number]; 129 | } 130 | 131 | type A1 = ApplyProjection; 132 | const a1: A1 = null as any; 133 | a1.id; 134 | type A2 = ApplyProjection; 135 | const a2: A2 = null as any; 136 | a2.record?.id; 137 | type A3 = ApplyProjection; 138 | const a3: A3 = null as any; 139 | a3.record?.id; 140 | 141 | type A4 = ApplyProjection; 142 | const a4: A4 = null as any; 143 | a4.items[0]; 144 | 145 | type A5 = ApplyProjection; 146 | const a5: A5 = null as any; 147 | a5.tuple[0]; 148 | a5.tuple[1]; 149 | 150 | type A6 = ApplyProjection; 151 | const a6: A6 = null as any; 152 | a6.tuple[0]; 153 | a6.tuple[1]; 154 | } 155 | 156 | const TableName = "test"; 157 | 158 | interface User { 159 | PK: `USER#${UserID}`; 160 | SK: `#PROFILE#${UserID}`; 161 | Username: string; 162 | FullName: string; 163 | Email: string; 164 | CreatedAt: Date; 165 | Address: string; 166 | } 167 | 168 | interface Order< 169 | UserID extends string = string, 170 | OrderID extends string = string 171 | > { 172 | PK: `USER#${UserID}`; 173 | SK: `ORDER#${OrderID}`; 174 | Username: string; 175 | /** 176 | * Order ID. 177 | */ 178 | OrderID: OrderID; 179 | Status: "PLACED" | "SHIPPED"; 180 | CreatedAt: Date; 181 | Address: string; 182 | } 183 | 184 | declare const client: TypeSafeDynamoDBv2; 185 | 186 | export async function getProfile(userId: String) { 187 | const profile = await client 188 | .getItem({ 189 | TableName, 190 | Key: { 191 | PK: { 192 | S: `USER#${userId}`, 193 | }, 194 | SK: { 195 | S: `#PROFILE#${userId}`, 196 | }, 197 | }, 198 | }) 199 | .promise(); 200 | 201 | profile.Item?.FullName; 202 | // @ts-expect-error 203 | profile.Item?.OrderID; 204 | } 205 | 206 | export async function getOrder(userId: string, orderId: string) { 207 | const order = await client 208 | .getItem({ 209 | TableName, 210 | Key: { 211 | PK: { 212 | S: `USER#${userId}`, 213 | }, 214 | SK: { 215 | S: `ORDER#${orderId}`, 216 | }, 217 | }, 218 | }) 219 | .promise(); 220 | 221 | order.Item?.OrderID; 222 | // @ts-expect-error 223 | order.Item?.FullName; 224 | } 225 | 226 | export async function updateOrder(userId: string, orderId: string) { 227 | const defaultBehavior = await client 228 | .updateItem({ 229 | TableName: "", 230 | Key: { 231 | PK: { 232 | S: `USER#${userId}`, 233 | }, 234 | SK: { 235 | S: `ORDER#${orderId}`, 236 | }, 237 | }, 238 | UpdateExpression: "#k = :v", 239 | ExpressionAttributeNames: { 240 | "#k": "list", 241 | }, 242 | ExpressionAttributeValues: { 243 | ":v": { 244 | S: "val", 245 | }, 246 | ":v2": { 247 | S: "val2", 248 | }, 249 | }, 250 | ConditionExpression: "#k = :v2", 251 | }) 252 | .promise(); 253 | // @ts-expect-error - default ReturnValues is None 254 | defaultBehavior.Attributes?.defaultBehavior.Attributes?.key; 255 | 256 | const returnNone = await client 257 | .updateItem({ 258 | TableName: "", 259 | Key: { 260 | PK: { 261 | S: `USER#${userId}`, 262 | }, 263 | SK: { 264 | S: `ORDER#${orderId}`, 265 | }, 266 | }, 267 | UpdateExpression: "a = 1", 268 | ReturnValues: "NONE", 269 | }) 270 | .promise(); 271 | // @ts-expect-error - nothing is Returned 272 | returnNone.Attributes.PK.S; 273 | 274 | const returnAllNew = await client 275 | .updateItem({ 276 | TableName: "", 277 | Key: { 278 | PK: { 279 | S: `USER#${userId}`, 280 | }, 281 | SK: { 282 | S: `ORDER#${orderId}`, 283 | }, 284 | }, 285 | UpdateExpression: "a = 1", 286 | ReturnValues: "ALL_NEW", 287 | }) 288 | .promise(); 289 | returnAllNew.Attributes?.PK; 290 | returnAllNew.Attributes?.OrderID; 291 | // @ts-expect-error 292 | returnAllNew.Attributes?.FullName; 293 | 294 | const returnAllOld = await client 295 | .updateItem({ 296 | TableName: "", 297 | Key: { 298 | PK: { 299 | S: `USER#${userId}`, 300 | }, 301 | SK: { 302 | S: `ORDER#${orderId}`, 303 | }, 304 | }, 305 | UpdateExpression: "a = 1", 306 | ReturnValues: "ALL_OLD", 307 | }) 308 | .promise(); 309 | returnAllOld.Attributes?.PK?.S; 310 | 311 | const returnUpdatedNew = await client 312 | .updateItem({ 313 | TableName: "", 314 | Key: { 315 | PK: { 316 | S: `USER#${userId}`, 317 | }, 318 | SK: { 319 | S: `ORDER#${orderId}`, 320 | }, 321 | }, 322 | UpdateExpression: "a = 1", 323 | ReturnValues: "UPDATED_NEW", 324 | }) 325 | .promise(); 326 | returnUpdatedNew.Attributes?.PK?.S; 327 | 328 | const returnUpdatedOld = await client 329 | .updateItem({ 330 | TableName: "", 331 | Key: { 332 | PK: { 333 | S: `USER#${userId}`, 334 | }, 335 | SK: { 336 | S: `ORDER#${orderId}`, 337 | }, 338 | }, 339 | UpdateExpression: "a = 1", 340 | ReturnValues: "UPDATED_OLD", 341 | }) 342 | .promise(); 343 | returnUpdatedOld.Attributes?.PK?.S; 344 | 345 | const narrowedType = await client 346 | .updateItem({ 347 | TableName: "", 348 | Key: { 349 | PK: { 350 | S: `USER#${userId}`, 351 | }, 352 | SK: { 353 | S: `ORDER#${orderId}`, 354 | }, 355 | }, 356 | UpdateExpression: "a = 1", 357 | ReturnValues: "UPDATED_OLD", 358 | }) 359 | .promise(); 360 | 361 | narrowedType.Attributes?.OrderID?.S; 362 | // @ts-expect-error - FullName does not exist on order 363 | narrowedType.Attributes?.FullName?.S; 364 | } 365 | -------------------------------------------------------------------------------- /test/v3-document.test.ts: -------------------------------------------------------------------------------- 1 | import "jest"; 2 | 3 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 4 | import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; 5 | import { TypeSafeDocumentClientV3 } from "../src/document-client-v3"; 6 | import { TypeSafePutDocumentCommand } from "../src/put-document-command"; 7 | import { TypeSafeUpdateDocumentCommand } from "../src/update-document-command"; 8 | import { TypeSafeQueryDocumentCommand } from "../src/query-document-command"; 9 | import { TypeSafeGetDocumentCommand } from "../src/get-document-command"; 10 | import { TypeSafeDeleteDocumentCommand } from "../src/delete-document-command"; 11 | 12 | interface MyType { 13 | key: string; 14 | sort: number; 15 | list: string[]; 16 | } 17 | 18 | const client = new DynamoDBClient({}); 19 | 20 | const docClient = DynamoDBDocumentClient.from( 21 | client 22 | ) as TypeSafeDocumentClientV3; 23 | 24 | const PutItemCommand = TypeSafePutDocumentCommand(); 25 | const UpdateItemCommand = TypeSafeUpdateDocumentCommand< 26 | MyType, 27 | "key", 28 | "sort" 29 | >(); 30 | const GetItemCommand = TypeSafeGetDocumentCommand(); 31 | const DeleteItemCommand = TypeSafeDeleteDocumentCommand< 32 | MyType, 33 | "key", 34 | "sort" 35 | >(); 36 | const QueryCommand = TypeSafeQueryDocumentCommand(); 37 | 38 | it("dummy", async () => { 39 | expect(1).toBe(1); 40 | }); 41 | 42 | export async function foo() { 43 | const get = await docClient.send( 44 | new GetItemCommand({ 45 | TableName: "", 46 | Key: { 47 | key: "", 48 | sort: 1, 49 | }, 50 | ProjectionExpression: "sort, key", 51 | }) 52 | ); 53 | get.Item?.key; 54 | // @ts-expect-error 55 | get.Item?.list; 56 | 57 | const getDoc = await docClient.get({ 58 | TableName: "", 59 | Key: { 60 | key: "", 61 | sort: 1, 62 | }, 63 | ProjectionExpression: "sort, key", 64 | }); 65 | getDoc.Item?.key; 66 | // @ts-expect-error 67 | getDoc.Item?.list; 68 | 69 | const put = await docClient.send( 70 | new PutItemCommand({ 71 | TableName: "", 72 | Item: { 73 | key: "", 74 | list: [], 75 | sort: 1, 76 | }, 77 | }) 78 | ); 79 | put.Attributes?.key; 80 | 81 | const putDoc = await docClient.put({ 82 | TableName: "", 83 | Item: { 84 | key: "", 85 | list: [], 86 | sort: 1, 87 | }, 88 | }); 89 | putDoc.Attributes?.key; 90 | 91 | const del = await docClient.send( 92 | new DeleteItemCommand({ 93 | TableName: "", 94 | Key: { 95 | key: "", 96 | // @ts-expect-error 97 | sort: "", 98 | }, 99 | }) 100 | ); 101 | del.Attributes?.key; 102 | 103 | const delDoc = await docClient.delete({ 104 | TableName: "", 105 | Key: { 106 | key: "", 107 | // @ts-expect-error 108 | sort: "", 109 | }, 110 | }); 111 | delDoc.Attributes?.key; 112 | 113 | const query = await docClient.send( 114 | new QueryCommand({ 115 | TableName: "", 116 | KeyConditionExpression: "#key = :val", 117 | AttributesToGet: ["key"], 118 | ExpressionAttributeNames: { 119 | "#key": "key", 120 | }, 121 | ExpressionAttributeValues: { 122 | ":val": "val", 123 | }, 124 | }) 125 | ); 126 | query.Items?.[0].key; 127 | 128 | const queryDoc = await docClient.query({ 129 | TableName: "", 130 | KeyConditionExpression: "#key = :val", 131 | AttributesToGet: ["key"], 132 | ExpressionAttributeNames: { 133 | "#key": "key", 134 | }, 135 | ExpressionAttributeValues: { 136 | ":val": "val", 137 | }, 138 | }); 139 | queryDoc.Items?.[0].key; 140 | } 141 | 142 | export async function updateItem() { 143 | const defaultBehavior = await docClient.send( 144 | new UpdateItemCommand({ 145 | TableName: "", 146 | Key: { 147 | key: "", 148 | sort: 1, 149 | }, 150 | UpdateExpression: "#k = :v, #k_v = :v_2", 151 | ExpressionAttributeNames: { 152 | "#k": "list", 153 | "#k_v": "list_v", 154 | }, 155 | ExpressionAttributeValues: { 156 | ":v": "val", 157 | ":v2": "val2", 158 | ":v_2": "val_2", 159 | }, 160 | ConditionExpression: "#k = :v2", 161 | }) 162 | ); 163 | // @ts-expect-error - default ReturnValues is None 164 | defaultBehavior.Attributes?.key; 165 | 166 | const returnNone = await docClient.send( 167 | new UpdateItemCommand({ 168 | TableName: "", 169 | Key: { 170 | key: "", 171 | sort: 1, 172 | }, 173 | UpdateExpression: "a = 1", 174 | ReturnValues: "NONE", 175 | }) 176 | ); 177 | // @ts-expect-error - nothing is Returned 178 | returnNone.Attributes?.key; 179 | 180 | const returnAllNew = await docClient.send( 181 | new UpdateItemCommand({ 182 | TableName: "", 183 | Key: { 184 | key: "", 185 | sort: 1, 186 | }, 187 | UpdateExpression: "a = 1", 188 | ReturnValues: "ALL_NEW", 189 | }) 190 | ); 191 | returnAllNew.Attributes?.key?.length; 192 | 193 | const returnAllOld = await docClient.send( 194 | new UpdateItemCommand({ 195 | TableName: "", 196 | Key: { 197 | key: "", 198 | sort: 1, 199 | }, 200 | UpdateExpression: "a = 1", 201 | ReturnValues: "ALL_OLD", 202 | }) 203 | ); 204 | returnAllOld.Attributes?.key?.length; 205 | 206 | const returnUpdatedNew = await docClient.send( 207 | new UpdateItemCommand({ 208 | TableName: "", 209 | Key: { 210 | key: "", 211 | sort: 1, 212 | }, 213 | UpdateExpression: "a = 1", 214 | ReturnValues: "UPDATED_NEW", 215 | }) 216 | ); 217 | returnUpdatedNew.Attributes?.key?.length; 218 | 219 | const returnUpdatedOld = await docClient.send( 220 | new UpdateItemCommand({ 221 | TableName: "", 222 | Key: { 223 | key: "", 224 | sort: 1, 225 | }, 226 | UpdateExpression: "a = 1", 227 | ReturnValues: "UPDATED_OLD", 228 | }) 229 | ); 230 | returnUpdatedOld.Attributes?.key?.length; 231 | } 232 | 233 | export async function updateItemDocClient() { 234 | const defaultBehavior = await docClient.update({ 235 | TableName: "", 236 | Key: { 237 | key: "", 238 | sort: 1, 239 | }, 240 | UpdateExpression: "#k = :v", 241 | ExpressionAttributeNames: { 242 | "#k": "list", 243 | }, 244 | ExpressionAttributeValues: { 245 | ":v": "val", 246 | ":v2": "val2", 247 | }, 248 | ConditionExpression: "#k = :v2", 249 | }); 250 | // @ts-expect-error - default ReturnValues is None 251 | defaultBehavior.Attributes?.key; 252 | 253 | const returnNone = await docClient.update({ 254 | TableName: "", 255 | Key: { 256 | key: "", 257 | sort: 1, 258 | }, 259 | UpdateExpression: "a = 1", 260 | ReturnValues: "NONE", 261 | }); 262 | // @ts-expect-error - nothing is Returned 263 | returnNone.Attributes?.key; 264 | 265 | const returnAllNew = await docClient.update({ 266 | TableName: "", 267 | Key: { 268 | key: "", 269 | sort: 1, 270 | }, 271 | UpdateExpression: "a = 1", 272 | ReturnValues: "ALL_NEW", 273 | }); 274 | returnAllNew.Attributes?.key?.length; 275 | 276 | const returnAllOld = await docClient.update({ 277 | TableName: "", 278 | Key: { 279 | key: "", 280 | sort: 1, 281 | }, 282 | UpdateExpression: "a = 1", 283 | ReturnValues: "ALL_OLD", 284 | }); 285 | returnAllOld.Attributes?.key?.length; 286 | 287 | const returnUpdatedNew = await docClient.update({ 288 | TableName: "", 289 | Key: { 290 | key: "", 291 | sort: 1, 292 | }, 293 | UpdateExpression: "a = 1", 294 | ReturnValues: "UPDATED_NEW", 295 | }); 296 | returnUpdatedNew.Attributes?.key?.length; 297 | 298 | const returnUpdatedOld = await docClient.update({ 299 | TableName: "", 300 | Key: { 301 | key: "", 302 | sort: 1, 303 | }, 304 | UpdateExpression: "a = 1", 305 | ReturnValues: "UPDATED_OLD", 306 | }); 307 | returnUpdatedOld.Attributes?.key?.length; 308 | } 309 | -------------------------------------------------------------------------------- /test/v3.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck - disabling so we can publish types that work with document client 2 | 3 | import "jest"; 4 | 5 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 6 | import { TypeSafeGetItemCommand } from "../src/get-item-command"; 7 | import { TypeSafeDeleteItemCommand } from "../src/delete-item-command"; 8 | import { TypeSafePutItemCommand } from "../src/put-item-command"; 9 | import { TypeSafeQueryCommand } from "../src/query-command"; 10 | import { TypeSafeUpdateItemCommand } from "../src/update-item-command"; 11 | 12 | interface MyType { 13 | key: string; 14 | sort: number; 15 | list: string[]; 16 | } 17 | 18 | const client = new DynamoDBClient({}); 19 | 20 | const PutItemCommand = TypeSafePutItemCommand(); 21 | const UpdateItemCommand = TypeSafeUpdateItemCommand(); 22 | const GetItemCommand = TypeSafeGetItemCommand(); 23 | const DeleteItemCommand = TypeSafeDeleteItemCommand(); 24 | const QueryCommand = TypeSafeQueryCommand(); 25 | 26 | it("dummy", () => { 27 | expect(1).toBe(1); 28 | }); 29 | 30 | export async function foo() { 31 | const getCommand = new GetItemCommand({ 32 | TableName: "", 33 | Key: { 34 | key: { 35 | S: "", 36 | }, 37 | sort: { 38 | N: "1", 39 | }, 40 | }, 41 | ProjectionExpression: "sort, key", 42 | }); 43 | const get = await client.send(getCommand); 44 | 45 | get.Item?.key; 46 | // @ts-expect-error 47 | get.Item?.list; 48 | 49 | const put = await client.send( 50 | new PutItemCommand({ 51 | TableName: "", 52 | Item: { 53 | key: { 54 | S: "", 55 | }, 56 | list: { 57 | L: [], 58 | }, 59 | sort: { 60 | N: "1", 61 | }, 62 | }, 63 | }) 64 | ); 65 | put.Attributes?.key; 66 | 67 | const del = await client.send( 68 | new DeleteItemCommand({ 69 | TableName: "", 70 | Key: { 71 | key: { 72 | S: "", 73 | }, 74 | sort: { 75 | // @ts-expect-error 76 | S: "", 77 | }, 78 | }, 79 | }) 80 | ); 81 | del.Attributes?.key; 82 | 83 | const query = await client.send( 84 | new QueryCommand({ 85 | TableName: "", 86 | KeyConditionExpression: "#key = :val", 87 | AttributesToGet: ["key"], 88 | ExpressionAttributeNames: { 89 | "#key": "key", 90 | }, 91 | ExpressionAttributeValues: { 92 | ":val": { 93 | S: "val", 94 | }, 95 | }, 96 | }) 97 | ); 98 | query.Items?.[0].key; 99 | } 100 | 101 | export async function updateItem() { 102 | const defaultBehavior = await client.send( 103 | new UpdateItemCommand({ 104 | TableName: "", 105 | Key: { 106 | key: { 107 | S: "", 108 | }, 109 | sort: { 110 | N: "1", 111 | }, 112 | }, 113 | UpdateExpression: "#k = :v", 114 | ExpressionAttributeNames: { 115 | "#k": "list", 116 | }, 117 | ExpressionAttributeValues: { 118 | ":v": { 119 | S: "val", 120 | }, 121 | ":v2": { 122 | S: "val2", 123 | }, 124 | }, 125 | ConditionExpression: "#k = :v2", 126 | }) 127 | ); 128 | // @ts-expect-error - default ReturnValues is None 129 | defaultBehavior.Attributes?.key; 130 | 131 | const returnNone = await client.send( 132 | new UpdateItemCommand({ 133 | TableName: "", 134 | Key: { 135 | key: { 136 | S: "", 137 | }, 138 | sort: { 139 | N: "1", 140 | }, 141 | }, 142 | UpdateExpression: "a = 1", 143 | ReturnValues: "NONE", 144 | }) 145 | ); 146 | // @ts-expect-error - nothing is Returned 147 | returnNone.Attributes?.key; 148 | 149 | const returnAllNew = await client.send( 150 | new UpdateItemCommand({ 151 | TableName: "", 152 | Key: { 153 | key: { 154 | S: "", 155 | }, 156 | sort: { 157 | N: "1", 158 | }, 159 | }, 160 | UpdateExpression: "a = 1", 161 | ReturnValues: "ALL_NEW", 162 | }) 163 | ); 164 | returnAllNew.Attributes?.key?.S; 165 | 166 | const returnAllOld = await client.send( 167 | new UpdateItemCommand({ 168 | TableName: "", 169 | Key: { 170 | key: { 171 | S: "", 172 | }, 173 | sort: { 174 | N: "1", 175 | }, 176 | }, 177 | UpdateExpression: "a = 1", 178 | ReturnValues: "ALL_OLD", 179 | }) 180 | ); 181 | returnAllOld.Attributes?.key?.S; 182 | 183 | const returnUpdatedNew = await client.send( 184 | new UpdateItemCommand({ 185 | TableName: "", 186 | Key: { 187 | key: { 188 | S: "", 189 | }, 190 | sort: { 191 | N: "1", 192 | }, 193 | }, 194 | UpdateExpression: "a = 1", 195 | ReturnValues: "UPDATED_NEW", 196 | }) 197 | ); 198 | returnUpdatedNew.Attributes?.key?.S; 199 | 200 | const returnUpdatedOld = await client.send( 201 | new UpdateItemCommand({ 202 | TableName: "", 203 | Key: { 204 | key: { 205 | S: "", 206 | }, 207 | sort: { 208 | N: "1", 209 | }, 210 | }, 211 | UpdateExpression: "a = 1", 212 | ReturnValues: "UPDATED_OLD", 213 | }) 214 | ); 215 | returnUpdatedOld.Attributes?.key?.S; 216 | } 217 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "dom" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.js" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "dom" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------