├── .all-contributorsrc ├── .commitlintrc.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── .huskyrc.yml ├── .lintstagedrc.yml ├── .nojekyll ├── .prettierrc.yml ├── .releaserc.yml ├── LICENSE ├── README.md ├── doc-intro.md ├── jest.config.js ├── package-lock.json ├── package.json ├── renovate.json ├── snippets ├── app.snippet.ts ├── config │ ├── date-mapper.snippet.ts │ ├── log-receiver.snippet.ts │ ├── session-validity-ensurer.snippet.ts │ └── table-name-resolver.snippet.ts ├── expressions │ ├── attribute2-condition.snippet.ts │ ├── or-not-and-condition.snippet.ts │ └── update-expression.snippet.ts ├── looks-easy-right.snippet.ts ├── models │ ├── another.model.ts │ ├── date-to-number.mapper.ts │ ├── index.ts │ ├── my-model-with-gsi.snippet.ts │ ├── my-model-with-lsi.snippet.ts │ ├── my-model-with-sort-key.snippet.ts │ ├── my-model-with-transient.snippet.ts │ ├── my-model.snippet.ts │ └── person.model.ts ├── multi-model-requests │ ├── batch-get.snippet.ts │ ├── batch-write.snippet.ts │ ├── transact-get.snippet.ts │ └── transact-write.snippet.ts ├── store-requests │ ├── batch-get.snippet.ts │ ├── batch-write.snippet.ts │ ├── delete.snippet.ts │ ├── get.snippet.ts │ ├── put.snippet.ts │ ├── query.snippet.ts │ ├── scan.snippet.ts │ ├── transact-get.snippet.ts │ ├── update-complex.snippet.ts │ └── update-simple.snippet.ts ├── store.snippet.ts └── tsconfig.json ├── src ├── config │ ├── config.type.ts │ ├── dynamo-easy-config.ts │ ├── public-api.ts │ ├── update-config.function.spec.ts │ └── update-config.function.ts ├── decorator │ ├── decorators.spec.ts │ ├── impl │ │ ├── collection │ │ │ ├── collection-property-data.model.ts │ │ │ ├── collection-property.decorator.spec.ts │ │ │ └── collection-property.decorator.ts │ │ ├── date │ │ │ ├── date-property-data.model.ts │ │ │ ├── date-property.decorator.spec.ts │ │ │ └── date-property.decorator.ts │ │ ├── index │ │ │ ├── gsi-partition-key.decorator.ts │ │ │ ├── gsi-sort-key.decorator.ts │ │ │ ├── index-type.enum.ts │ │ │ ├── lsi-sort-key.decorator.ts │ │ │ ├── secondary-index.ts │ │ │ └── util.ts │ │ ├── key │ │ │ ├── partition-key.decorator.ts │ │ │ └── sort-key.decorator.ts │ │ ├── model │ │ │ ├── errors.const.ts │ │ │ ├── key-model.const.ts │ │ │ ├── model-data.model.ts │ │ │ ├── model.decorator.spec.ts │ │ │ └── model.decorator.ts │ │ ├── property │ │ │ ├── init-or-update-property.function.ts │ │ │ ├── key-property.const.ts │ │ │ ├── property-data.model.ts │ │ │ └── property.decorator.ts │ │ ├── public-api.ts │ │ └── transient │ │ │ └── transient.decorator.ts │ ├── metadata │ │ ├── metadata-for-model.function.ts │ │ ├── metadata-helper.spec.ts │ │ ├── metadata.spec.ts │ │ ├── metadata.ts │ │ ├── model-metadata.model.ts │ │ ├── property-metadata.model.ts │ │ ├── property-metadata.spec.ts │ │ └── public-api.ts │ ├── public-api.ts │ └── util.ts ├── dynamo-easy.ts ├── dynamo │ ├── batchget │ │ ├── batch-get-full.response.ts │ │ ├── batch-get-utils.spec.ts │ │ ├── batch-get-utils.ts │ │ ├── batch-get.const.ts │ │ ├── batch-get.request.spec.ts │ │ ├── batch-get.request.ts │ │ ├── batch-get.response.ts │ │ └── public-api.ts │ ├── batchwrite │ │ ├── batch-write-utils.spec.ts │ │ ├── batch-write-utils.ts │ │ ├── batch-write.const.ts │ │ ├── batch-write.request.spec.ts │ │ ├── batch-write.request.ts │ │ └── public-api.ts │ ├── default-session-validity-ensurer.const.spec.ts │ ├── default-session-validity-ensurer.const.ts │ ├── default-table-name-resolver.const.ts │ ├── dynamo-api-operations.type.ts │ ├── dynamo-db-wrapper.spec.ts │ ├── dynamo-db-wrapper.ts │ ├── dynamo-store.spec.ts │ ├── dynamo-store.ts │ ├── expression │ │ ├── condition-expression-builder.spec.ts │ │ ├── condition-expression-builder.ts │ │ ├── create-if-not-exists-condition.function.spec.ts │ │ ├── create-if-not-exists-condition.function.ts │ │ ├── function-operators.const.ts │ │ ├── functions │ │ │ ├── alias-for-operator.function.ts │ │ │ ├── attribute-name-replacer.function.ts │ │ │ ├── attribute-name-replacer.spec.ts │ │ │ ├── attribute-names.const.ts │ │ │ ├── attribute-names.function.ts │ │ │ ├── is-function-operator.function.ts │ │ │ ├── is-no-param-function-operator.function.ts │ │ │ ├── operator-for-alias.function.ts │ │ │ ├── operator-parameter-arity.function.ts │ │ │ ├── resolve-attribute-value-name-conflicts.function.ts │ │ │ └── unique-attribute-value-name.function.ts │ │ ├── logical-operator │ │ │ ├── and.function.ts │ │ │ ├── attribute.function.spec.ts │ │ │ ├── attribute.function.ts │ │ │ ├── merge-conditions.function.spec.ts │ │ │ ├── merge-conditions.function.ts │ │ │ ├── not.function.ts │ │ │ ├── or.function.ts │ │ │ ├── public.api.ts │ │ │ └── update.function.ts │ │ ├── non-param-function-operators.const.ts │ │ ├── param-util.spec.ts │ │ ├── param-util.ts │ │ ├── prepare-and-add-update-expressions.function.spec.ts │ │ ├── prepare-and-add-update-expressions.function.ts │ │ ├── public-api.ts │ │ ├── request-expression-builder.spec.ts │ │ ├── request-expression-builder.ts │ │ ├── type │ │ │ ├── comparator-operator.type.ts │ │ │ ├── condition-expression-definition-chain.ts │ │ │ ├── condition-expression-definition-function.ts │ │ │ ├── condition-operator-alias.type.ts │ │ │ ├── condition-operator-to-alias-map.const.ts │ │ │ ├── condition-operator.type.ts │ │ │ ├── expression-type.type.ts │ │ │ ├── expression.type.ts │ │ │ ├── function-operator.type.ts │ │ │ ├── public-api.ts │ │ │ ├── sort-key-condition-function.ts │ │ │ ├── update-action-def.ts │ │ │ ├── update-action-defs.const.ts │ │ │ ├── update-action-keyword.type.ts │ │ │ ├── update-action.type.ts │ │ │ ├── update-expression-definition-chain.ts │ │ │ ├── update-expression-definition-function.ts │ │ │ └── update-expression.type.ts │ │ ├── update-action-keywords.const.ts │ │ ├── update-expression-builder.spec.ts │ │ ├── update-expression-builder.ts │ │ ├── util.spec.ts │ │ └── util.ts │ ├── get-table-name.function.spec.ts │ ├── get-table-name.function.ts │ ├── operation-params.type.ts │ ├── public-api.ts │ ├── request │ │ ├── base.request.spec.ts │ │ ├── base.request.ts │ │ ├── batchgetsingletable │ │ │ ├── batch-get-single-table.request.spec.ts │ │ │ ├── batch-get-single-table.request.ts │ │ │ └── batch-get-single-table.response.ts │ │ ├── batchwritesingletable │ │ │ ├── batch-write-single-table.request.spec.ts │ │ │ └── batch-write-single-table.request.ts │ │ ├── class-diagram.monopic │ │ ├── class-diagram.txt │ │ ├── delete │ │ │ ├── delete.request.spec.ts │ │ │ ├── delete.request.ts │ │ │ └── delete.response.ts │ │ ├── get │ │ │ ├── get.request.spec.ts │ │ │ ├── get.request.ts │ │ │ └── get.response.ts │ │ ├── helper │ │ │ ├── add-projection-expression-param.function.spec.ts │ │ │ └── add-projection-expression-param.function.ts │ │ ├── public-api.ts │ │ ├── put │ │ │ ├── put.request.spec.ts │ │ │ ├── put.request.ts │ │ │ └── put.response.ts │ │ ├── query │ │ │ ├── query.request.spec.ts │ │ │ ├── query.request.ts │ │ │ └── query.response.ts │ │ ├── read-many.request.spec.ts │ │ ├── read-many.request.ts │ │ ├── scan │ │ │ ├── scan.request.spec.ts │ │ │ ├── scan.request.ts │ │ │ └── scan.response.ts │ │ ├── standard.request.spec.ts │ │ ├── standard.request.ts │ │ ├── transactgetsingletable │ │ │ ├── transact-get-single-table.request.spec.ts │ │ │ ├── transact-get-single-table.request.ts │ │ │ └── transact-get-single-table.response.ts │ │ ├── update │ │ │ ├── update.request.spec.ts │ │ │ ├── update.request.ts │ │ │ └── update.response.ts │ │ ├── write.request.spec.ts │ │ └── write.request.ts │ ├── session-validity-ensurer.type.ts │ ├── table-name-resolver.type.ts │ ├── transactget │ │ ├── public-api.ts │ │ ├── transact-get-full.response.ts │ │ ├── transact-get.request.spec.ts │ │ ├── transact-get.request.ts │ │ └── transact-get.request.type.ts │ └── transactwrite │ │ ├── public-api.ts │ │ ├── transact-base-operation.spec.ts │ │ ├── transact-base-operation.ts │ │ ├── transact-condition-check.spec.ts │ │ ├── transact-condition-check.ts │ │ ├── transact-delete.spec.ts │ │ ├── transact-delete.ts │ │ ├── transact-operation.type.ts │ │ ├── transact-put.spec.ts │ │ ├── transact-put.ts │ │ ├── transact-update.spec.ts │ │ ├── transact-update.ts │ │ ├── transact-write.request.spec.ts │ │ └── transact-write.request.ts ├── helper │ ├── curry.function.spec.ts │ ├── curry.function.ts │ ├── extract-list-type.type.ts │ ├── fetch-all.function.ts │ ├── fetch-all.spec.ts │ ├── get-tag.function.ts │ ├── is-boolean.function.ts │ ├── is-boolean.spec.ts │ ├── is-empty.function.ts │ ├── is-empty.spec.ts │ ├── is-number.function.ts │ ├── is-number.spec.ts │ ├── is-plain-object.function.spec.ts │ ├── is-plain-object.function.ts │ ├── is-string.function.ts │ ├── is-string.spec.ts │ ├── kebab-case.function.spec.ts │ ├── kebab-case.function.ts │ ├── not-null.function.ts │ ├── promise-delay.function.spec.ts │ ├── promise-delay.function.ts │ ├── promise-tap.function.spec.ts │ ├── promise-tap.function.ts │ ├── random-exponential-backoff-timer.generator.spec.ts │ ├── random-exponential-backoff-timer.generator.ts │ └── tag.enum.ts ├── logger │ ├── default-log-receiver.const.ts │ ├── log-info.type.ts │ ├── log-level.type.ts │ ├── log-receiver.type.ts │ ├── logger.spec.ts │ ├── logger.ts │ └── public-api.ts ├── mapper │ ├── custom │ │ ├── date-to-number.mapper.spec.ts │ │ ├── date-to-number.mapper.ts │ │ ├── date-to-string.mapper.spec.ts │ │ └── date-to-string.mapper.ts │ ├── for-type │ │ ├── base.mapper.ts │ │ ├── boolean.mapper.spec.ts │ │ ├── boolean.mapper.ts │ │ ├── collection.mapper.spec.ts │ │ ├── collection.mapper.ts │ │ ├── enum.mapper.spec.ts │ │ ├── enum.mapper.ts │ │ ├── null.mapper.spec.ts │ │ ├── null.mapper.ts │ │ ├── number.mapper.spec.ts │ │ ├── number.mapper.ts │ │ ├── object.mapper.spec.ts │ │ ├── object.mapper.ts │ │ ├── string.mapper.spec.ts │ │ └── string.mapper.ts │ ├── mapper.spec.ts │ ├── mapper.ts │ ├── public-api.ts │ ├── type │ │ ├── attribute-type.type.ts │ │ ├── attribute-value-type.type.ts │ │ ├── attribute.type.ts │ │ ├── binary.type.ts │ │ ├── null.type.ts │ │ └── undefined.type.ts │ ├── util.spec.ts │ ├── util.ts │ ├── wrap-mapper-for-collection.function.spec.ts │ └── wrap-mapper-for-collection.function.ts └── model │ ├── model-constructor.ts │ ├── omit.type.ts │ └── public-api.ts ├── test ├── data │ ├── organization-dynamodb.data.ts │ └── product-dynamodb.data.ts ├── helper │ ├── get-meta-data-property.function.ts │ └── resetDynamoEasyConfig.function.ts ├── jest-setup.ts └── models │ ├── brutalist.model.ts │ ├── char-array.mapper.ts │ ├── complex.model.ts │ ├── custom-table-name.model.ts │ ├── duration.model.ts │ ├── employee.model.ts │ ├── fail-model.model.ts │ ├── index.ts │ ├── model-with-collections.model.ts │ ├── model-with-custom-mapper-and-default-value.model.ts │ ├── model-with-custom-mapper-for-sort-key.model.ts │ ├── model-with-custom-mapper.model.ts │ ├── model-with-date-as-key.model.ts │ ├── model-with-date.model.ts │ ├── model-with-default-value.model.ts │ ├── model-with-empty-values.ts │ ├── model-with-enum.model.ts │ ├── model-with-indexes.model.ts │ ├── model-with-nested-model-with-custom-mapper.model.ts │ ├── model-without-custom-mapper.model.ts │ ├── model-without-partition-key.model.ts │ ├── nested-complex.model.ts │ ├── nested-model-with-date.model.ts │ ├── nested-object.model.ts │ ├── organization.model.ts │ ├── product.model.ts │ ├── real-world │ ├── base-form.model.ts │ ├── extended-form.model.ts │ ├── form-id.model.ts │ ├── form-type.enum.ts │ ├── form.model.ts │ ├── index.ts │ ├── order-id.model.ts │ ├── order.model.ts │ └── product-base-form.model.ts │ ├── simple-with-composite-partition-key.model.ts │ ├── simple-with-partition-key.model.ts │ ├── simple.model.ts │ ├── special-cases-model.model.ts │ ├── types.enum.ts │ └── update.model.ts ├── tools ├── gh-pages-publish.ts └── tslint │ ├── noDynamoNamedImportRule.js │ ├── noDynamoNamedImportRule.ts │ └── test │ ├── test.ts.lint │ └── tslint.json ├── tsconfig.jest.json ├── tsconfig.json └── tslint.yml /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "dynamo-easy", 3 | "projectOwner": "shiftcode", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "michaelwittwer", 12 | "name": "Michael Wittwer", 13 | "avatar_url": "https://avatars1.githubusercontent.com/u/8394182?v=4", 14 | "profile": "https://www.shiftcode.ch", 15 | "contributions": [ 16 | "ideas", 17 | "code", 18 | "test", 19 | "doc" 20 | ] 21 | }, 22 | { 23 | "login": "simonmumenthaler", 24 | "name": "Simon Mumenthaler", 25 | "avatar_url": "https://avatars3.githubusercontent.com/u/37636934?v=4", 26 | "profile": "https://github.com/simonmumenthaler", 27 | "contributions": [ 28 | "ideas", 29 | "code", 30 | "test", 31 | "doc", 32 | "examples" 33 | ] 34 | }, 35 | { 36 | "login": "michaellieberherrr", 37 | "name": "Michael Lieberherr", 38 | "avatar_url": "https://avatars1.githubusercontent.com/u/8321523?v=4", 39 | "profile": "https://github.com/michaellieberherrr", 40 | "contributions": [ 41 | "code", 42 | "doc", 43 | "test" 44 | ] 45 | } 46 | ], 47 | "repoType": "github" 48 | } 49 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 120 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Lint, Test, Build and optionally Publish 2 | 3 | on: 4 | # push only for branches (ignore tags) 5 | push: 6 | branches: 7 | - '**' 8 | tags-ignore: 9 | - '**' 10 | # pull request only for branches (ignore tags) 11 | pull_request: 12 | branches: 13 | - '**' 14 | tags-ignore: 15 | - '**' 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2.2.0 22 | - name: Install Node v12 23 | uses: actions/setup-node@v2-beta 24 | with: 25 | node-version: 12 26 | - uses: actions/cache@v2 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | - name: install 33 | run: npm ci 34 | - name: lint, test, and build 35 | run: | 36 | npm run lint:ci 37 | npm run test:ci 38 | npm run build 39 | env: 40 | CI: true 41 | # report coverage only for non PR 42 | - name: coveralls 43 | if: ${{ startsWith(github.ref, 'refs/pull/') == false }} 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | # publish to latest if on master branch 49 | - name: release master 50 | if: ${{ github.ref == 'master' }} 51 | run: | 52 | npm run docs:build 53 | npx semantic-release 54 | npm run docs:deploy 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | 59 | # publish pre-release if non master branch and allowed by .releaserc.yml configuration (only for non-PR branches) 60 | - name: release non-master version 61 | if: ${{ github.ref != 'master' && startsWith(github.ref, 'refs/pull/') == false }} 62 | run: npx semantic-release 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache -------------------------------------------------------------------------------- /.huskyrc.yml: -------------------------------------------------------------------------------- 1 | hooks: 2 | commit-msg: "commitlint -E HUSKY_GIT_PARAMS" 3 | pre-commit: "lint-staged" 4 | pre-push: "npm run check-snippets && npm run test && npm run build" 5 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "src/**/!(*.spec).ts": 2 | - prettier --write --config ./.prettierrc.yml 3 | - tslint --project ./tsconfig.json -t codeFrame --fix 4 | "(src/**/*.spec.ts|test/**/*.ts)": 5 | - prettier --write --config ./.prettierrc.yml 6 | # TODO LOW tslint will not work because of the following error 7 | # ✖ tslint --project ./tsconfig.jest.json -t codeFrame --fix found some errors. Please fix them and try committing again. 8 | #'/Users/michaelwittwer/dev/shiftcode/dynamo-easy/test/models/complex.model.ts' is not included in project. 9 | # - tslint --project ./tsconfig.jest.json -t codeFrame --fix 10 | "**/package.json": 11 | - sort-package-json 12 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiftcode/dynamo-easy/2e1ad7c0bc41eeb8a45ba873d17e9d998fdbf41c/.nojekyll -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/prettier/prettier/blob/master/docs/options.md 2 | printWidth: 120 3 | singleQuote: true 4 | semi: false 5 | trailingComma: all 6 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | - name: master 3 | - name: next 4 | channel: next 5 | prerelease: next 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 shiftcode GmbH (info@shiftcode.ch) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc-intro.md: -------------------------------------------------------------------------------- 1 | 2 | ### Config 3 | * [updateDynamoEasyConfig](modules/dynamo_easy.html#updatedynamoeasyconfig) 4 | * [tableNameResolver](modules/dynamo_easy.html#tablenameresolver) 5 | * [sessionValidityEnsurer](modules/dynamo_easy.html#sessionvalidityensurer) 6 | 7 | ### Model 8 | * [Decorators](modules/decorators.html) 9 | * [MapperForType](interfaces/mapper.mapperfortype.html) 10 | 11 | ### DynamoStore 12 | * [DynamoStore](classes/dynamo_easy.dynamostore.html) 13 | 14 | #### Store Requests (Single Model) 15 | * [GetRequest](classes/store_requests.getrequest.html) 16 | * [PutRequest](classes/store_requests.putrequest.html) 17 | * [DeleteRequest](classes/store_requests.deleterequest.html) 18 | * [UpdateRequest](classes/store_requests.updaterequest.html) 19 | * [QueryRequest](classes/store_requests.queryrequest.html) 20 | * [ScanRequest](classes/store_requests.scanrequest.html) 21 | * [TransactGetSingleTableRequest](classes/store_requests.transactgetsingletablerequest.html) 22 | * [BatchGetSingleTableRequest](classes/store_requests.batchgetsingletablerequest.html) 23 | * [BatchWriteSingleTableRequest](classes/store_requests.batchwritesingletablerequest.html) 24 | 25 | ### Multi Model Requests 26 | * [BatchGetRequest](classes/multi_model_requests_batch_get.batchgetrequest.html) 27 | * [BatchWriteRequest](classes/multi_model_requests_batch_write.batchwriterequest.html) 28 | * [TransactGetRequest](classes/multi_model_requests_transact_get.transactgetrequest.html) 29 | * [TransactWriteRequest](classes/multi_model_requests_transact_write.transactwriterequest.html) 30 | 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveragePathIgnorePatterns: [ 3 | "/node_modules/", 4 | "/test/" 5 | ], 6 | coverageThreshold: { 7 | global: { 8 | branches: 10, 9 | functions: 10, 10 | lines: 10, 11 | statements: 10 12 | } 13 | }, 14 | globals: { 15 | "ts-jest": { 16 | diagnostics: { 17 | ignoreCodes: [151001] 18 | }, 19 | tsConfig: "./tsconfig.jest.json" 20 | } 21 | }, 22 | moduleFileExtensions: [ 23 | "ts", 24 | "tsx", 25 | "js" 26 | ], 27 | setupFiles: [ 28 | "reflect-metadata", 29 | './test/jest-setup.ts' 30 | ], 31 | testEnvironment: "node", 32 | testRegex: "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 33 | transform: { 34 | ".(ts|tsx|js)": "ts-jest" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ], 6 | "ignoreDeps": [ 7 | "@types/node", 8 | "typescript", 9 | "typedoc", 10 | "typedoc-plugin-external-module-name" 11 | ], 12 | "packageRules": [ 13 | { 14 | "packagePatterns": [ "^@commitlint" ], 15 | "groupName": "@commitlint" 16 | }, 17 | { 18 | "packagePatterns": ["jest"], 19 | "groupName": "jest" 20 | }, 21 | { 22 | "packagePatterns": ["uuid"], 23 | "groupName": "uuid" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /snippets/app.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import * as AWS from 'aws-sdk/global' 3 | import { Person } from './models' 4 | 5 | // update the aws config with your credentials to enable successful connection 6 | AWS.config.update({ region: 'yourAwsRegion' }) 7 | 8 | const personStore = new DynamoStore(Person) 9 | 10 | // add a new item 11 | personStore.put({ id: 'wernerv', name: 'Werner Hans Peter Vogels', yearOfBirth: 1958 }) 12 | .exec() 13 | .then(() => { 14 | console.log('person stored') 15 | }) 16 | 17 | // search for a single person by known id 18 | personStore.query() 19 | .wherePartitionKey('wernerv') 20 | .execSingle() 21 | .then(person => { 22 | console.log('got person', person) 23 | }) 24 | 25 | // returns all persons where the name starts with w 26 | personStore.scan() 27 | .whereAttribute('name').beginsWith('w') 28 | .exec() 29 | .then((persons: Person[]) => { 30 | console.log('all persons', persons) 31 | }) 32 | -------------------------------------------------------------------------------- /snippets/config/date-mapper.snippet.ts: -------------------------------------------------------------------------------- 1 | import { updateDynamoEasyConfig } from '@shiftcoders/dynamo-easy' 2 | import { dateToNumberMapper } from '../models' 3 | 4 | updateDynamoEasyConfig({ 5 | dateMapper: dateToNumberMapper 6 | }) 7 | -------------------------------------------------------------------------------- /snippets/config/log-receiver.snippet.ts: -------------------------------------------------------------------------------- 1 | import { LogInfo, updateDynamoEasyConfig } from '@shiftcoders/dynamo-easy' 2 | 3 | updateDynamoEasyConfig({ 4 | logReceiver: (logInfo: LogInfo) => { 5 | const msg = `[${logInfo.level}] ${logInfo.timestamp} ${logInfo.className} (${ 6 | logInfo.modelConstructor 7 | }): ${logInfo.message}` 8 | console.debug(msg, logInfo.data) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /snippets/config/session-validity-ensurer.snippet.ts: -------------------------------------------------------------------------------- 1 | import { updateDynamoEasyConfig } from '@shiftcoders/dynamo-easy' 2 | 3 | updateDynamoEasyConfig({ 4 | sessionValidityEnsurer: (): Promise => { 5 | // do whatever you need to do to make sure the session is valid 6 | // and return an Promise when done 7 | return Promise.resolve() 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /snippets/config/table-name-resolver.snippet.ts: -------------------------------------------------------------------------------- 1 | import { TableNameResolver, updateDynamoEasyConfig } from '@shiftcoders/dynamo-easy' 2 | 3 | const myTableNameResolver: TableNameResolver = (tableName: string) => { 4 | return `myPrefix-${tableName}` 5 | } 6 | 7 | updateDynamoEasyConfig({ 8 | tableNameResolver: myTableNameResolver 9 | }) 10 | -------------------------------------------------------------------------------- /snippets/expressions/attribute2-condition.snippet.ts: -------------------------------------------------------------------------------- 1 | import { attribute2, DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | const personStore = new DynamoStore(Person) 5 | 6 | personStore.delete('volgelsw') 7 | .onlyIf( 8 | attribute2(Person, 'yearOfBirth').eq(1958), 9 | ) 10 | -------------------------------------------------------------------------------- /snippets/expressions/or-not-and-condition.snippet.ts: -------------------------------------------------------------------------------- 1 | import { and, attribute, DynamoStore, not, or } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | const personStore = new DynamoStore(Person) 5 | personStore.delete('vogelsw') 6 | .onlyIf( 7 | or( 8 | and( 9 | attribute('myProp').eq('foo'), 10 | attribute('otherProp').eq('bar') 11 | ), 12 | not( 13 | attribute('otherProp').eq('foo bar') 14 | ), 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /snippets/expressions/update-expression.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore, update } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | const personStore = new DynamoStore(Person) 5 | personStore.update('vogelsw') 6 | .operations( 7 | update('name').set('Werner Vogels'), 8 | update('yearOfBirth').set(1984), 9 | ) 10 | -------------------------------------------------------------------------------- /snippets/looks-easy-right.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from './models' 3 | 4 | const personStore = new DynamoStore(Person) 5 | 6 | personStore 7 | .scan() 8 | .whereAttribute('yearOfBirth').equals(1958) 9 | .exec() 10 | .then(res => console.log('ALL items with yearOfBirth == 1958', res)) 11 | -------------------------------------------------------------------------------- /snippets/models/another.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, DateProperty, Model, PartitionKey, SortKey } from '@shiftcoders/dynamo-easy' 2 | 3 | @Model() 4 | export class AnotherModel { 5 | @PartitionKey() 6 | propA: string 7 | 8 | @SortKey() 9 | propB: string 10 | 11 | 12 | propC?: string 13 | 14 | @CollectionProperty() 15 | myNestedList?: any[] 16 | 17 | @DateProperty() 18 | updated: Date 19 | } 20 | -------------------------------------------------------------------------------- /snippets/models/date-to-number.mapper.ts: -------------------------------------------------------------------------------- 1 | import { MapperForType, NumberAttribute } from '@shiftcoders/dynamo-easy' 2 | 3 | export const dateToNumberMapper: MapperForType = { 4 | fromDb: attributeValue => new Date(parseInt(attributeValue.N, 10)), 5 | toDb: propertyValue => ({ N: `${propertyValue.getTime()}` }), 6 | } 7 | -------------------------------------------------------------------------------- /snippets/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './another.model' 2 | export * from './person.model' 3 | export * from './date-to-number.mapper' 4 | -------------------------------------------------------------------------------- /snippets/models/my-model-with-gsi.snippet.ts: -------------------------------------------------------------------------------- 1 | import { Model, GSIPartitionKey, GSISortKey } from '@shiftcoders/dynamo-easy' 2 | 3 | const MY_MODEL_GSI = 'NameOfGSI' 4 | 5 | @Model() 6 | class MyModel { 7 | @GSIPartitionKey(MY_MODEL_GSI) 8 | myGsiPartitionKey: string 9 | 10 | @GSISortKey(MY_MODEL_GSI) 11 | myGsiSortKey: number 12 | } 13 | -------------------------------------------------------------------------------- /snippets/models/my-model-with-lsi.snippet.ts: -------------------------------------------------------------------------------- 1 | import { Model, LSISortKey, PartitionKey, SortKey } from '@shiftcoders/dynamo-easy' 2 | 3 | @Model() 4 | class MyModel { 5 | @PartitionKey() 6 | myPartitionKey: string 7 | 8 | @SortKey() 9 | mySortKey: number 10 | 11 | @LSISortKey('NameOfLSI') 12 | myLsiSortKey: number 13 | } 14 | -------------------------------------------------------------------------------- /snippets/models/my-model-with-sort-key.snippet.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, SortKey } from '@shiftcoders/dynamo-easy' 2 | 3 | @Model() 4 | export class MyModel { 5 | @PartitionKey() 6 | myPartitionKey: string 7 | 8 | @SortKey() 9 | mySortKey: number 10 | } 11 | -------------------------------------------------------------------------------- /snippets/models/my-model-with-transient.snippet.ts: -------------------------------------------------------------------------------- 1 | import { Model, Transient } from '@shiftcoders/dynamo-easy' 2 | 3 | @Model() 4 | class MyModel { 5 | @Transient() 6 | myPropertyToIgnore: any 7 | } 8 | -------------------------------------------------------------------------------- /snippets/models/my-model.snippet.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unnecessary-class 2 | import { Model } from '@shiftcoders/dynamo-easy' 3 | 4 | @Model({ tableName: 'my-model-table-name' }) 5 | export class MyModel { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /snippets/models/person.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey } from '@shiftcoders/dynamo-easy' 2 | 3 | @Model() 4 | export class Person { 5 | @PartitionKey() 6 | id: string 7 | name: string 8 | yearOfBirth: number 9 | } 10 | -------------------------------------------------------------------------------- /snippets/multi-model-requests/batch-get.snippet.ts: -------------------------------------------------------------------------------- 1 | import { BatchGetRequest, BatchGetResponse } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel, Person } from '../models' 3 | 4 | const keysToFetch: Array> = [{ id: 'vogelsw' }] 5 | const otherKeysToFetch: Array> = [{ propA: 'Foo', propB: 'Bar' }] 6 | 7 | new BatchGetRequest() 8 | .forModel(Person, keysToFetch) 9 | .forModel(AnotherModel, otherKeysToFetch) 10 | .exec() 11 | .then((result: BatchGetResponse) => { 12 | console.log('items fetched from example table', result.tableNameOfExampleModel) 13 | console.log('items fetched from another table', result.tableNameOfAnotherModel) 14 | }) 15 | -------------------------------------------------------------------------------- /snippets/multi-model-requests/batch-write.snippet.ts: -------------------------------------------------------------------------------- 1 | import { BatchWriteRequest } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel, Person } from '../models' 3 | 4 | const keysToDelete: Array> = [{ id: 'vogelsw' }] 5 | const otherKeysToDelete: Array> = [{ propA: 'Foo', propB: 'Bar' }] 6 | const objectsToPut: AnotherModel[] = [ 7 | { propA: 'foo', propB: 'bar', updated: new Date() }, 8 | { propA: 'foo2', propB: 'bar2', updated: new Date() }, 9 | ] 10 | 11 | new BatchWriteRequest() 12 | .returnConsumedCapacity('TOTAL') 13 | .delete(Person, keysToDelete) 14 | .delete(AnotherModel, otherKeysToDelete) 15 | .put(AnotherModel, objectsToPut) 16 | .execFullResponse() 17 | .then(resp => { 18 | console.log(resp.ConsumedCapacity) 19 | }) 20 | -------------------------------------------------------------------------------- /snippets/multi-model-requests/transact-get.snippet.ts: -------------------------------------------------------------------------------- 1 | import { TransactGetRequest } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel, Person } from '../models' 3 | 4 | new TransactGetRequest() 5 | .returnConsumedCapacity('TOTAL') 6 | .forModel(Person, { id: 'vogelsw' }) 7 | .forModel(AnotherModel, { propA: 'Foo', propB: 'Bar' }) 8 | .exec() 9 | .then(result => { 10 | console.log(result[0]) // Person item 11 | console.log(result[1]) // AnotherModel item 12 | }) 13 | -------------------------------------------------------------------------------- /snippets/multi-model-requests/transact-write.snippet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | attribute, 3 | TransactConditionCheck, 4 | TransactDelete, 5 | TransactPut, 6 | TransactUpdate, 7 | TransactWriteRequest, 8 | } from '@shiftcoders/dynamo-easy' 9 | import { AnotherModel, Person } from '../models' 10 | 11 | const objectToPut: AnotherModel = { propA: 'Foo', propB: 'Bar', updated: new Date() } 12 | 13 | new TransactWriteRequest() 14 | .transact( 15 | new TransactConditionCheck(Person, 'vogelsw').onlyIf(attribute('yearOfBirth').gte(1958)), 16 | new TransactDelete(AnotherModel, 'Foo', 'Bar'), 17 | new TransactPut(AnotherModel, objectToPut), 18 | new TransactUpdate(Person, 'vogelsw').updateAttribute('yearOfBirth').set(1984), 19 | ) 20 | .exec() 21 | .then(() => console.log('done')) 22 | -------------------------------------------------------------------------------- /snippets/store-requests/batch-get.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .batchGet([{ id: 'a' }, { id: 'b' }]) 6 | .exec() 7 | .then(res => console.log('fetched items:', res)) 8 | -------------------------------------------------------------------------------- /snippets/store-requests/batch-write.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .batchWrite() 6 | .delete([{ id: 'a' }, { id: 'b' }]) 7 | .put([{ id: 'vogelsw', name: 'Werner Hans Peter Vogels', yearOfBirth: 1958 }]) 8 | .exec() 9 | .then(() => console.log('item a, b deleted; werner vogels added')) 10 | -------------------------------------------------------------------------------- /snippets/store-requests/delete.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .delete('vogelsw') 6 | .onlyIfAttribute('yearOfBirth').lte(1958) 7 | .exec() 8 | .then(() => console.log('done')) 9 | -------------------------------------------------------------------------------- /snippets/store-requests/get.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .get('wernerv') // returns an instance of GetRequest 6 | .consistentRead(true) // sets params.ConsistentRead = true 7 | .exec() // returns a Promise 8 | .then(obj => console.log(obj)) 9 | -------------------------------------------------------------------------------- /snippets/store-requests/put.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | const objectToPut: Person = { 5 | id: 'vogelsw', 6 | name: 'Werner Hans Peter Vogels', 7 | yearOfBirth: 1958, 8 | } // object literal or new Person(...) 9 | 10 | new DynamoStore(Person) 11 | .put(objectToPut) 12 | .ifNotExists() 13 | .exec() 14 | .then(() => console.log('done')) 15 | -------------------------------------------------------------------------------- /snippets/store-requests/query.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel } from '../models' 3 | 4 | new DynamoStore(AnotherModel) 5 | .query() 6 | .wherePartitionKey('2018-01') 7 | .whereSortKey().beginsWith('a') 8 | .execSingle() 9 | .then(r => console.log('first found item:', r)) 10 | -------------------------------------------------------------------------------- /snippets/store-requests/scan.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .scan() 6 | .whereAttribute('yearOfBirth').equals(1958) 7 | .execFetchAll() 8 | .then(res => console.log('ALL items with yearOfBirth == 1958', res)) 9 | -------------------------------------------------------------------------------- /snippets/store-requests/transact-get.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from '../models' 3 | 4 | new DynamoStore(Person) 5 | .transactGet([{ id: 'a' }, { id: 'b' }]) 6 | .exec() 7 | .then(() => console.log('transactionally read a and b')) 8 | -------------------------------------------------------------------------------- /snippets/store-requests/update-complex.snippet.ts: -------------------------------------------------------------------------------- 1 | import { attribute, DynamoStore, or, update } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel } from '../models' 3 | 4 | const index = 3 5 | const oneHourAgo = new Date(Date.now() - 1000 * 60 * 60) 6 | 7 | new DynamoStore(AnotherModel) 8 | .update('myPartitionKey', 'mySortKey') 9 | .operations( 10 | update(`myNestedList[${index}].propertyX`).set('value'), 11 | update('updated').set(new Date()), 12 | ) 13 | .onlyIf( 14 | or( 15 | attribute('id').attributeNotExists(), // item not existing 16 | attribute('updated').lt(oneHourAgo), // or was not updated in the last hour 17 | ), 18 | ) 19 | .returnValues('ALL_OLD') 20 | .exec() 21 | .then(oldVal => console.log('old value was:', oldVal)) 22 | -------------------------------------------------------------------------------- /snippets/store-requests/update-simple.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { AnotherModel } from '../models' 3 | 4 | const oneHourAgo = new Date(Date.now() - 1000 * 60 * 60) 5 | 6 | new DynamoStore(AnotherModel) 7 | .update('myPartitionKey', 'mySortKey') 8 | .updateAttribute('propC').set('newValue') 9 | .updateAttribute('updated').set(new Date()) 10 | .onlyIfAttribute('updated').lt(oneHourAgo) 11 | .exec() 12 | .then(() => console.log('done')) 13 | -------------------------------------------------------------------------------- /snippets/store.snippet.ts: -------------------------------------------------------------------------------- 1 | import { DynamoStore } from '@shiftcoders/dynamo-easy' 2 | import { Person } from './models' 3 | 4 | const personStore = new DynamoStore(Person) 5 | -------------------------------------------------------------------------------- /snippets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "es2015", 5 | "module": "es2015", 6 | "lib": [ 7 | "es2015", 8 | "es2016.array.include", 9 | "es2017.object" 10 | ], 11 | "strict": true, 12 | "strictPropertyInitialization": false, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "skipLibCheck": true, 17 | "types": [ 18 | "node", 19 | "reflect-metadata" 20 | ], 21 | "paths": { 22 | "@shiftcoders/*": [ 23 | "../src/*" 24 | ] 25 | } 26 | }, 27 | "include": [ 28 | "./**/*.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/config/config.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import { SessionValidityEnsurer } from '../dynamo/session-validity-ensurer.type' 5 | import { TableNameResolver } from '../dynamo/table-name-resolver.type' 6 | import { LogReceiver } from '../logger/log-receiver.type' 7 | import { MapperForType } from '../mapper/for-type/base.mapper' 8 | 9 | /** 10 | * the global config object 11 | */ 12 | export interface Config { 13 | /** 14 | * function receiving all the log statements 15 | */ 16 | logReceiver: LogReceiver 17 | /** 18 | * mapper used for {@link DateProperty} decorated properties 19 | */ 20 | dateMapper: MapperForType 21 | /** 22 | * function used to create the table names 23 | */ 24 | tableNameResolver: TableNameResolver 25 | /** 26 | * function called before calling dynamoDB api 27 | */ 28 | sessionValidityEnsurer: SessionValidityEnsurer 29 | } 30 | -------------------------------------------------------------------------------- /src/config/dynamo-easy-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import { DEFAULT_SESSION_VALIDITY_ENSURER } from '../dynamo/default-session-validity-ensurer.const' 5 | import { DEFAULT_TABLE_NAME_RESOLVER } from '../dynamo/default-table-name-resolver.const' 6 | import { DEFAULT_LOG_RECEIVER } from '../logger/default-log-receiver.const' 7 | import { dateToStringMapper } from '../mapper/custom/date-to-string.mapper' 8 | import { Config } from './config.type' 9 | 10 | /** 11 | * to update the config you must do it before importing any model, basically before anything else. 12 | * the config cannot be changed afterwards 13 | */ 14 | export const dynamoEasyConfig: Config = { 15 | dateMapper: dateToStringMapper, 16 | logReceiver: DEFAULT_LOG_RECEIVER, 17 | tableNameResolver: DEFAULT_TABLE_NAME_RESOLVER, 18 | sessionValidityEnsurer: DEFAULT_SESSION_VALIDITY_ENSURER, 19 | } 20 | -------------------------------------------------------------------------------- /src/config/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | export * from './config.type' 5 | export * from './update-config.function' 6 | 7 | // do not export dynamo-easy-config.ts 8 | -------------------------------------------------------------------------------- /src/config/update-config.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import { Config } from './config.type' 5 | import { dynamoEasyConfig } from './dynamo-easy-config' 6 | 7 | /** 8 | * update the global dynamoEasy {@link Config} 9 | */ 10 | export function updateDynamoEasyConfig(config: Partial): void { 11 | if ('logReceiver' in config && typeof config.logReceiver !== 'function') { 12 | throw new Error('Config.logReceiver has to be a function') 13 | } 14 | if ( 15 | 'dateMapper' in config && 16 | (!config.dateMapper || 17 | typeof config.dateMapper.toDb !== 'function' || 18 | typeof config.dateMapper.fromDb !== 'function') 19 | ) { 20 | throw new Error('Config.dateMapper must be an object of type MapperForType') 21 | } 22 | if ('tableNameResolver' in config && typeof config.tableNameResolver !== 'function') { 23 | throw new Error('Config.tableNameResolver must be function') 24 | } 25 | if ('sessionValidityEnsurer' in config && typeof config.sessionValidityEnsurer !== 'function') { 26 | throw new Error('Config.sessionValidityEnsurer must be function') 27 | } 28 | Object.assign(dynamoEasyConfig, config) 29 | } 30 | -------------------------------------------------------------------------------- /src/decorator/impl/collection/collection-property-data.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { MapperForType } from '../../../mapper/for-type/base.mapper' 5 | import { BinaryAttribute, NumberAttribute, StringAttribute } from '../../../mapper/type/attribute.type' 6 | import { ModelConstructor } from '../../../model/model-constructor' 7 | 8 | /** 9 | * Option interface for @CollectionProperty decorator 10 | */ 11 | export interface CollectionPropertyData { 12 | /** 13 | * the name of property how it is named in dynamoDB 14 | */ 15 | name?: string 16 | 17 | /** 18 | * if the collection should preserve the order. if so it will be stored as (L)ist 19 | */ 20 | sorted?: boolean 21 | 22 | /** 23 | * provide an itemMapper if you want your complex items being mapped to String|Number|Binary attribute 24 | * (e.g. because you can't store it in a (S)et otherwise) 25 | * only provide either itemMapper or itemType --> not both 26 | * itemMapper is basically intended to be used with [S]et. though it also works with [L]ist 27 | */ 28 | itemMapper?: MapperForType 29 | 30 | /** 31 | * provide an itemType (class with @Model decorator) for complex types (eg. Set) 32 | * collections with complex types will be stored as (L)ist 33 | * only provide either itemType or itemMapper --> not both 34 | */ 35 | itemType?: ModelConstructor 36 | } 37 | -------------------------------------------------------------------------------- /src/decorator/impl/date/date-property-data.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * Option interface for @DateProperty decorator 6 | */ 7 | export interface DatePropertyData { 8 | // the name of property how it is named in dynamoDB 9 | name: string 10 | } 11 | -------------------------------------------------------------------------------- /src/decorator/impl/date/date-property.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-non-null-assertion 2 | import { getMetaDataProperty } from '../../../../test/helper/get-meta-data-property.function' 3 | import { resetDynamoEasyConfig } from '../../../../test/helper/resetDynamoEasyConfig.function' 4 | import { updateDynamoEasyConfig } from '../../../config/update-config.function' 5 | import { dateToNumberMapper } from '../../../mapper/custom/date-to-number.mapper' 6 | import { metadataForModel } from '../../metadata/metadata-for-model.function' 7 | import { ModelMetadata } from '../../metadata/model-metadata.model' 8 | import { Model } from '../model/model.decorator' 9 | import { DateProperty } from './date-property.decorator' 10 | 11 | @Model() 12 | class ModelWithDate { 13 | @DateProperty() 14 | aDate: Date 15 | } 16 | 17 | describe('Date decorators should allow to use a different date mapper', () => { 18 | beforeEach(() => updateDynamoEasyConfig({ dateMapper: dateToNumberMapper })) 19 | afterEach(resetDynamoEasyConfig) 20 | 21 | it('should define the dateToNumberMapper in metadata', () => { 22 | const metaData: ModelMetadata = metadataForModel(ModelWithDate).modelOptions 23 | 24 | expect(metaData).toBeDefined() 25 | expect(metaData.clazz).toBe(ModelWithDate) 26 | expect(metaData.properties).toBeDefined() 27 | 28 | const nameProp = getMetaDataProperty(metaData, 'aDate') 29 | 30 | expect(nameProp).toBeDefined() 31 | expect(nameProp!.name).toBe('aDate') 32 | expect(nameProp!.mapper).toBeDefined() 33 | expect(nameProp!.mapper!()).toBe(dateToNumberMapper) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/decorator/impl/date/date-property.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { dynamoEasyConfig } from '../../../config/dynamo-easy-config' 5 | import { PropertyMetadata } from '../../metadata/property-metadata.model' 6 | import { initOrUpdateProperty } from '../property/init-or-update-property.function' 7 | import { DatePropertyData } from './date-property-data.model' 8 | 9 | export function DateProperty(opts: Partial = {}): PropertyDecorator { 10 | return (target: any, propertyKey: string | symbol) => { 11 | if (typeof propertyKey === 'string') { 12 | const propertyOptions: Partial> = { 13 | name: propertyKey, 14 | nameDb: opts.name || propertyKey, 15 | mapper: () => dynamoEasyConfig.dateMapper, 16 | } 17 | 18 | initOrUpdateProperty(propertyOptions, target, propertyKey) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/decorator/impl/index/gsi-partition-key.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { IndexType } from './index-type.enum' 5 | import { initOrUpdateIndex } from './util' 6 | 7 | /** 8 | * decorator to use property as GSI partition key 9 | */ 10 | export function GSIPartitionKey(indexName: string): PropertyDecorator { 11 | return (target: any, propertyKey: string | symbol) => { 12 | if (typeof propertyKey === 'string') { 13 | initOrUpdateIndex(IndexType.GSI, { name: indexName, keyType: 'HASH' }, target, propertyKey) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/decorator/impl/index/gsi-sort-key.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { IndexType } from './index-type.enum' 5 | import { initOrUpdateIndex } from './util' 6 | 7 | /** 8 | * decorator to use property as GSI sort key 9 | */ 10 | export function GSISortKey(indexName: string): PropertyDecorator { 11 | return (target: any, propertyKey: string | symbol) => { 12 | if (typeof propertyKey === 'string') { 13 | initOrUpdateIndex(IndexType.GSI, { name: indexName, keyType: 'RANGE' }, target, propertyKey) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/decorator/impl/index/index-type.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export enum IndexType { 8 | GSI, 9 | LSI, 10 | } 11 | -------------------------------------------------------------------------------- /src/decorator/impl/index/lsi-sort-key.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { IndexType } from './index-type.enum' 5 | import { initOrUpdateIndex } from './util' 6 | 7 | /** 8 | * Marks a property as the sort key attribute of a local secondary index (the partition key must be same as in base table) 9 | */ 10 | export function LSISortKey(indexName: string): PropertyDecorator { 11 | return (target: any, propertyKey: string | symbol) => { 12 | if (typeof propertyKey === 'string') { 13 | initOrUpdateIndex(IndexType.LSI, { name: indexName, keyType: 'RANGE' }, target, propertyKey) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/decorator/impl/index/secondary-index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export interface SecondaryIndex { 8 | partitionKey: keyof T 9 | sortKey?: keyof T 10 | } 11 | -------------------------------------------------------------------------------- /src/decorator/impl/key/partition-key.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { createOptModelLogger } from '../../../logger/logger' 5 | import { PropertyMetadata } from '../../metadata/property-metadata.model' 6 | import { initOrUpdateProperty } from '../property/init-or-update-property.function' 7 | import { KEY_PROPERTY } from '../property/key-property.const' 8 | 9 | const logger = createOptModelLogger('@PartitionKey') 10 | 11 | export function PartitionKey(): PropertyDecorator { 12 | return (target: any, propertyKey: string | symbol) => { 13 | if (typeof propertyKey === 'string') { 14 | // check for existing properties marked as partition key 15 | const properties: Array> = Reflect.getMetadata(KEY_PROPERTY, target.constructor) || [] 16 | if (properties && properties.length) { 17 | const existingPartitionKeys = properties.filter((property) => property.key && property.key.type === 'HASH') 18 | if (existingPartitionKeys.length) { 19 | if (properties.find((property) => property.name === propertyKey)) { 20 | // just ignore this and go on, somehow the partition key gets defined two times 21 | logger.warn( 22 | `this is the second execution to define the partitionKey for property ${propertyKey}`, 23 | target.constructor, 24 | ) 25 | } else { 26 | throw new Error( 27 | 'only one partition key is allowed per model, if you want to define key for indexes use one of these decorators: ' + 28 | '@GSIPartitionKey, @GSISortKey or @LSISortKey', 29 | ) 30 | } 31 | } 32 | } 33 | 34 | initOrUpdateProperty({ key: { type: 'HASH' } }, target, propertyKey) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/decorator/impl/key/sort-key.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { initOrUpdateProperty } from '../property/init-or-update-property.function' 5 | 6 | export function SortKey(): PropertyDecorator { 7 | return (target: any, propertyKey: string | symbol) => { 8 | if (typeof propertyKey === 'string') { 9 | initOrUpdateProperty({ key: { type: 'RANGE' } }, target, propertyKey) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/decorator/impl/model/errors.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | 5 | /** 6 | * @hidden 7 | */ 8 | export const modelErrors = { 9 | gsiMultiplePk: (indexName: string, propDbName: string) => 10 | `there is already a partition key defined for global secondary index ${indexName} (property name: ${propDbName})`, 11 | gsiMultipleSk: (indexName: string, propDbName: string) => 12 | `there is already a sort key defined for global secondary index ${indexName} (property name: ${propDbName})`, 13 | lsiMultipleSk: (indexName: string, propDbName: string) => 14 | `only one sort key can be defined for the same local secondary index, ${propDbName} is already defined as sort key for index ${indexName}`, 15 | lsiRequiresPk: (indexName: string, propDbName: string) => 16 | `the local secondary index ${indexName} requires the partition key to be defined`, 17 | } 18 | -------------------------------------------------------------------------------- /src/decorator/impl/model/key-model.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export const KEY_MODEL = 'sc-reflect:model' 8 | -------------------------------------------------------------------------------- /src/decorator/impl/model/model-data.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * Option interface for @Model decorator 6 | */ 7 | export interface ModelData { 8 | tableName?: string 9 | } 10 | -------------------------------------------------------------------------------- /src/decorator/impl/property/init-or-update-property.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { Attribute } from '../../../mapper/type/attribute.type' 5 | import { ModelConstructor } from '../../../model/model-constructor' 6 | import { PropertyMetadata, TypeInfo } from '../../metadata/property-metadata.model' 7 | import { getMetadataType } from '../../util' 8 | import { KEY_PROPERTY } from './key-property.const' 9 | 10 | /** 11 | * @hidden 12 | */ 13 | export function initOrUpdateProperty( 14 | propertyMetadata: Partial> = {}, 15 | target: any, 16 | propertyKey: string, 17 | ): void { 18 | // the new or updated property 19 | let property: PropertyMetadata 20 | 21 | // Update the attribute array 22 | let properties: Array> = Reflect.getMetadata(KEY_PROPERTY, target.constructor) || [] 23 | const existingProperty = properties.find((p) => p.name === propertyKey) 24 | if (existingProperty) { 25 | // create new property with merged options 26 | property = { ...existingProperty, ...propertyMetadata } 27 | // remove existing from array 28 | properties = properties.filter((p) => p !== existingProperty) 29 | } else { 30 | // add new options 31 | property = createNewProperty(propertyMetadata, target, propertyKey) 32 | } 33 | 34 | Reflect.defineMetadata(KEY_PROPERTY, [...properties, property], target.constructor) 35 | } 36 | 37 | /** 38 | * @hidden 39 | */ 40 | function createNewProperty( 41 | propertyOptions: Partial> = {}, 42 | target: any, 43 | propertyKey: string, 44 | ): PropertyMetadata { 45 | const propertyType: ModelConstructor = getMetadataType(target, propertyKey) 46 | 47 | if (propertyType === undefined) { 48 | throw new Error( 49 | 'make sure you have enabled the typescript compiler options which enable us to work with decorators (see doc)', 50 | ) 51 | } 52 | 53 | const typeInfo: TypeInfo = { type: propertyType } 54 | 55 | propertyOptions = { 56 | name: propertyKey, 57 | nameDb: propertyKey, 58 | typeInfo, 59 | ...propertyOptions, 60 | } 61 | 62 | return >propertyOptions 63 | } 64 | -------------------------------------------------------------------------------- /src/decorator/impl/property/key-property.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export const KEY_PROPERTY = 'sc-reflect:property' 8 | -------------------------------------------------------------------------------- /src/decorator/impl/property/property-data.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { MapperForType } from '../../../mapper/for-type/base.mapper' 5 | 6 | /** 7 | * Option interface for @Property decorator 8 | */ 9 | export interface PropertyData { 10 | /** 11 | * the name of property how it is named in dynamoDB 12 | */ 13 | name: string 14 | mapper: MapperForType 15 | defaultValueProvider: () => T 16 | } 17 | -------------------------------------------------------------------------------- /src/decorator/impl/property/property.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { PropertyMetadata } from '../../metadata/property-metadata.model' 5 | import { initOrUpdateProperty } from './init-or-update-property.function' 6 | import { PropertyData } from './property-data.model' 7 | 8 | export function Property(opts: Partial> = {}): PropertyDecorator { 9 | return (target: object, propertyKey: string | symbol) => { 10 | if (typeof propertyKey === 'string') { 11 | const propertyOptions: Partial> = { 12 | name: propertyKey, 13 | nameDb: opts.name || propertyKey, 14 | defaultValueProvider: opts.defaultValueProvider, 15 | } 16 | 17 | if ('mapper' in opts && !!opts.mapper) { 18 | const m = opts.mapper 19 | propertyOptions.mapper = () => m 20 | } 21 | 22 | initOrUpdateProperty(propertyOptions, target, propertyKey) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/decorator/impl/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | // collection 5 | export * from './collection/collection-property.decorator' 6 | 7 | // date 8 | export * from './date/date-property.decorator' 9 | // index 10 | export * from './index/secondary-index' 11 | export * from './index/gsi-partition-key.decorator' 12 | export * from './index/gsi-sort-key.decorator' 13 | export * from './index/lsi-sort-key.decorator' 14 | export * from './index/index-type.enum' 15 | // key 16 | export * from './key/partition-key.decorator' 17 | export * from './key/sort-key.decorator' 18 | 19 | // model 20 | export * from './model/model.decorator' 21 | // property 22 | export * from './property/property.decorator' 23 | // transient 24 | export * from './transient/transient.decorator' 25 | -------------------------------------------------------------------------------- /src/decorator/impl/transient/transient.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | import { initOrUpdateProperty } from '../property/init-or-update-property.function' 5 | 6 | export function Transient(): PropertyDecorator { 7 | return (target: any, propertyKey: string | symbol) => { 8 | if (typeof propertyKey === 'string') { 9 | initOrUpdateProperty({ transient: true }, target, propertyKey) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/decorator/metadata/metadata-for-model.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module metadata 3 | */ 4 | import { ModelConstructor } from '../../model/model-constructor' 5 | import { Metadata } from './metadata' 6 | 7 | /** 8 | * create the metadata wrapper instance for a @Model() decorated class. 9 | */ 10 | export function metadataForModel(modelConstructor: ModelConstructor): Metadata { 11 | return new Metadata(modelConstructor) 12 | } 13 | -------------------------------------------------------------------------------- /src/decorator/metadata/metadata-helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Model } from '../impl/model/model.decorator' 2 | import { Property } from '../impl/property/property.decorator' 3 | import { metadataForModel } from './metadata-for-model.function' 4 | 5 | @Model() 6 | class ModelWithMetadata { 7 | @Property({ name: 'myProp' }) 8 | prop: string 9 | } 10 | 11 | describe('metadata helper', () => { 12 | it('should return metadata using either name or namedb as property name', () => { 13 | const modelOptionsWithName = metadataForModel(ModelWithMetadata).forProperty('prop') 14 | expect(modelOptionsWithName).toBeDefined() 15 | const modelOptionsWithNameDb = metadataForModel(ModelWithMetadata).forProperty('myProp') 16 | expect(modelOptionsWithNameDb).toBeDefined() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/decorator/metadata/model-metadata.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module metadata 3 | */ 4 | import { SecondaryIndex } from '../impl/index/secondary-index' 5 | import { PropertyMetadata } from './property-metadata.model' 6 | 7 | /** 8 | * Options provided to model decorator annotation 9 | */ 10 | export interface ModelMetadata { 11 | clazzName: string 12 | clazz: any 13 | tableName: string 14 | properties: Array> 15 | transientProperties?: Array 16 | 17 | // local and global secondary indexes maps the name to the index definition (partition and optional sort key depending on index type) 18 | indexes: Map> 19 | } 20 | -------------------------------------------------------------------------------- /src/decorator/metadata/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module metadata 3 | */ 4 | export * from './metadata' 5 | export * from './metadata-for-model.function' 6 | export * from './model-metadata.model' 7 | export * from './property-metadata.model' 8 | -------------------------------------------------------------------------------- /src/decorator/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | // 5 | // impl 6 | // 7 | export * from './impl/public-api' 8 | // 9 | // metadata 10 | // 11 | export * from './metadata/public-api' 12 | // 13 | // util & helpers 14 | // 15 | export * from './util' 16 | -------------------------------------------------------------------------------- /src/decorator/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module decorators 3 | */ 4 | // these reflection keys are built in using the reflect-metadata library 5 | import { ModelConstructor } from '../model/model-constructor' 6 | 7 | /** 8 | * @hidden 9 | */ 10 | export const KEY_TYPE = 'design:type' 11 | 12 | /** 13 | * @hidden 14 | */ 15 | export const KEY_PARAMETER = 'design:paramtypes' 16 | 17 | /** 18 | * @hidden 19 | */ 20 | export const KEY_RETURN_TYPE = 'design:returntype' 21 | 22 | /** 23 | * @hidden 24 | */ 25 | export const getMetadataType = makeMetadataGetter>(KEY_TYPE) 26 | 27 | /** 28 | * @hidden 29 | */ 30 | export function makeMetadataGetter(metadataKey: string): (target: any, targetKey: string) => T { 31 | return (target: any, targetKey: string) => Reflect.getMetadata(metadataKey, target, targetKey) 32 | } 33 | -------------------------------------------------------------------------------- /src/dynamo-easy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | // 5 | // Export public api of the library 6 | // 7 | export * from './config/public-api' 8 | export * from './decorator/public-api' 9 | export * from './dynamo/public-api' 10 | export * from './logger/public-api' 11 | export * from './mapper/public-api' 12 | export * from './model/public-api' 13 | -------------------------------------------------------------------------------- /src/dynamo/batchget/batch-get-full.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-get 3 | */ 4 | 5 | // tslint:disable-next-line:interface-over-type-literal 6 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 7 | import { BatchGetResponse } from './batch-get.response' 8 | 9 | /** 10 | * Response from {@link BatchGetRequest}::execFullResponse 11 | */ 12 | export interface BatchGetFullResponse { 13 | /** 14 | * A map of table name to a list of items. Each object in Responses consists of a table name, along with a map of attribute data consisting of the data type and attribute value. 15 | */ 16 | Responses: BatchGetResponse 17 | /** 18 | * A map of tables and their respective keys that were not processed with the current response. The UnprocessedKeys value is in the same form as RequestItems, so the value can be provided directly to a subsequent BatchGetItem operation. For more information, see RequestItems in the Request Parameters section. Each element consists of: Keys - An array of primary key attribute values that define specific items in the table. ProjectionExpression - One or more attributes to be retrieved from the table or index. By default, all attributes are returned. If a requested attribute is not found, it does not appear in the result. ConsistentRead - The consistency of a read operation. If set to true, then a strongly consistent read is used; otherwise, an eventually consistent read is used. If there are no unprocessed keys remaining, the response contains an empty UnprocessedKeys map. 19 | */ 20 | UnprocessedKeys?: DynamoDB.BatchGetRequestMap 21 | /** 22 | * The read capacity units consumed by the entire BatchGetItem operation. Each element consists of: TableName - The table that consumed the provisioned throughput. CapacityUnits - The total number of capacity units consumed. 23 | */ 24 | ConsumedCapacity?: DynamoDB.ConsumedCapacityMultiple 25 | } 26 | -------------------------------------------------------------------------------- /src/dynamo/batchget/batch-get.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-get 3 | */ 4 | /** 5 | * max count of request items allowed by aws 6 | */ 7 | export const BATCH_GET_MAX_REQUEST_ITEM_COUNT = 100 8 | 9 | /** 10 | * time slot used to wait after unprocessed items were returned 11 | */ 12 | export const BATCH_GET_DEFAULT_TIME_SLOT = 1000 13 | -------------------------------------------------------------------------------- /src/dynamo/batchget/batch-get.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-get 3 | */ 4 | /** 5 | * Response from {@link BatchGetRequest}::exec 6 | */ 7 | // tslint:disable-next-line:interface-over-type-literal 8 | export type BatchGetResponse = Record 9 | -------------------------------------------------------------------------------- /src/dynamo/batchget/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-get 3 | */ 4 | export * from './batch-get.request' 5 | export * from './batch-get.response' 6 | export * from './batch-get-full.response' 7 | export * from './batch-get.const' 8 | -------------------------------------------------------------------------------- /src/dynamo/batchwrite/batch-write.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-write 3 | */ 4 | 5 | /** 6 | * max count of request items allowed by aws 7 | */ 8 | export const BATCH_WRITE_MAX_REQUEST_ITEM_COUNT = 25 9 | 10 | /** 11 | * time slot used to wait after unprocessed items were returned 12 | */ 13 | export const BATCH_WRITE_DEFAULT_TIME_SLOT = 1000 14 | -------------------------------------------------------------------------------- /src/dynamo/batchwrite/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/batch-write 3 | */ 4 | export * from './batch-write.request' 5 | export * from './batch-write.const' 6 | -------------------------------------------------------------------------------- /src/dynamo/default-session-validity-ensurer.const.spec.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SESSION_VALIDITY_ENSURER } from './default-session-validity-ensurer.const' 2 | 3 | describe('DEFAULT_SESSION_VALIDITY_ENSURER', () => { 4 | it('should return a promise without value', async () => { 5 | expect(typeof DEFAULT_SESSION_VALIDITY_ENSURER === 'function').toBeTruthy() 6 | expect(DEFAULT_SESSION_VALIDITY_ENSURER() instanceof Promise).toBeTruthy() 7 | expect(await DEFAULT_SESSION_VALIDITY_ENSURER()).toBeUndefined() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/dynamo/default-session-validity-ensurer.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import { SessionValidityEnsurer } from './session-validity-ensurer.type' 5 | 6 | /** 7 | * A simple no-op function which tells that we always have a valid session, which obviously requires some valid 8 | * session checking and also renewing of a potentially expired (or non existing) session 9 | */ 10 | export const DEFAULT_SESSION_VALIDITY_ENSURER: SessionValidityEnsurer = () => Promise.resolve() 11 | -------------------------------------------------------------------------------- /src/dynamo/default-table-name-resolver.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | /** 5 | * simple no-op function to always use table name without modification 6 | */ 7 | export const DEFAULT_TABLE_NAME_RESOLVER = (tableName: string) => tableName 8 | -------------------------------------------------------------------------------- /src/dynamo/dynamo-api-operations.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type DynamoApiOperations = 8 | | 'batchGetItem' 9 | | 'batchWriteItem' 10 | | 'createTable' 11 | | 'deleteItem' 12 | | 'deleteTable' 13 | | 'describeLimits' 14 | | 'describeTable' 15 | | 'describeTimeToLive' 16 | | 'getItem' 17 | | 'listTables' 18 | | 'listTagsOfResource' 19 | | 'putItem' 20 | | 'query' 21 | | 'scan' 22 | | 'tagResource' 23 | | 'untagResource' 24 | | 'updateItem' 25 | | 'updateTable' 26 | | 'updateTimeToLive' 27 | -------------------------------------------------------------------------------- /src/dynamo/expression/create-if-not-exists-condition.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { hasSortKey, Metadata } from '../../decorator/metadata/metadata' 5 | import { attribute } from './logical-operator/public.api' 6 | import { ConditionExpressionDefinitionFunction } from './type/condition-expression-definition-function' 7 | 8 | /** 9 | * @hidden 10 | */ 11 | export function createIfNotExistsCondition(metadata: Metadata): ConditionExpressionDefinitionFunction[] { 12 | const conditionDefFns: ConditionExpressionDefinitionFunction[] = [ 13 | attribute(metadata.getPartitionKey()).attributeNotExists(), 14 | ] 15 | if (hasSortKey(metadata)) { 16 | conditionDefFns.push(attribute(metadata.getSortKey()).attributeNotExists()) 17 | } 18 | return conditionDefFns 19 | } 20 | -------------------------------------------------------------------------------- /src/dynamo/expression/function-operators.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { FunctionOperator } from './type/function-operator.type' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const FUNCTION_OPERATORS: FunctionOperator[] = [ 10 | 'attribute_exists', 11 | 'attribute_not_exists', 12 | 'attribute_type', 13 | 'begins_with', 14 | 'contains', 15 | 'not_contains', 16 | 'IN', 17 | 'BETWEEN', 18 | ] 19 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/alias-for-operator.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { OperatorAlias } from '../type/condition-operator-alias.type' 5 | import { OPERATOR_TO_ALIAS_MAP } from '../type/condition-operator-to-alias-map.const' 6 | import { ConditionOperator } from '../type/condition-operator.type' 7 | 8 | /** 9 | * @hidden 10 | */ 11 | export function aliasForOperator(operator: ConditionOperator): OperatorAlias { 12 | return Array.isArray(OPERATOR_TO_ALIAS_MAP[operator]) 13 | ? OPERATOR_TO_ALIAS_MAP[operator][0] 14 | : OPERATOR_TO_ALIAS_MAP[operator] 15 | } 16 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/attribute-name-replacer.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export function attributeNameReplacer(substring: string, ...args: any[]): string { 8 | return `_at_${args[0]}` 9 | } 10 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/attribute-name-replacer.spec.ts: -------------------------------------------------------------------------------- 1 | import { attributeNameReplacer } from './attribute-name-replacer.function' 2 | import { BRACED_INDEX_REGEX } from './unique-attribute-value-name.function' 3 | 4 | describe('attribute value replaces', () => { 5 | it('should replace', () => { 6 | const attrPath = 'list[0]' 7 | expect(attrPath.replace(BRACED_INDEX_REGEX, attributeNameReplacer)).toBe('list_at_0') 8 | }) 9 | 10 | it('should replace 2', () => { 11 | const attrPath = 'list[0].ages[2]' 12 | expect(attrPath.replace(BRACED_INDEX_REGEX, attributeNameReplacer)).toBe('list_at_0.ages_at_2') 13 | }) 14 | 15 | it('should replace 3', () => { 16 | const attrPath = 'attr.persons[0].age' 17 | expect(attrPath.replace(BRACED_INDEX_REGEX, attributeNameReplacer)).toBe('attr.persons_at_0.age') 18 | }) 19 | 20 | it('should replace 4', () => { 21 | const attrPath = 'attr[2].persons[0].age' 22 | expect(attrPath.replace(BRACED_INDEX_REGEX, attributeNameReplacer)).toBe('attr_at_2.persons_at_0.age') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/attribute-names.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export const NESTED_ATTR_PATH_CAPTURED_REGEX = /([a-z0-9A-Z_]+)(?:\[(\d+)])?\.?/g 8 | /** 9 | * @hidden 10 | */ 11 | export const NESTED_ATTR_PATH_REGEX = /^.+((\[(\d+)])|(\.)).*$/ 12 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/attribute-names.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Metadata } from '../../../decorator/metadata/metadata' 5 | import { NESTED_ATTR_PATH_CAPTURED_REGEX, NESTED_ATTR_PATH_REGEX } from './attribute-names.const' 6 | 7 | // problem: we only get the metadata from the last property -> but we need it for all properties in the chain (prop1.prop2.prop3) 8 | /** 9 | * @hidden 10 | */ 11 | export function resolveAttributeNames( 12 | attributePath: string, 13 | metadata?: Metadata | undefined, 14 | ): { placeholder: string; attributeNames: Record } { 15 | let placeholder: string 16 | // tslint:disable-next-line:no-shadowed-variable 17 | const attributeNames: Record = {} 18 | if (new RegExp(NESTED_ATTR_PATH_REGEX).test(attributePath)) { 19 | const regex = new RegExp(NESTED_ATTR_PATH_CAPTURED_REGEX) 20 | // nested attribute with document path (map or list) 21 | const currentPath = [] 22 | let regExpResult: RegExpExecArray | null 23 | const namePlaceholders: string[] = [] 24 | // tslint:disable-next-line:no-conditional-assignment 25 | while ((regExpResult = regex.exec(attributePath)) !== null) { 26 | // path part is pos 1 - full match would be 0 27 | const pathPart = regExpResult[1] 28 | currentPath.push(regExpResult[1]) 29 | const collectionIndex = regExpResult[2] 30 | 31 | const propertyMetadata = metadata && metadata.forProperty(currentPath.join('.')) 32 | 33 | attributeNames[`#${pathPart}`] = propertyMetadata ? propertyMetadata.nameDb : pathPart 34 | if (collectionIndex !== undefined) { 35 | namePlaceholders.push(`#${pathPart}[${collectionIndex}]`) 36 | } else { 37 | namePlaceholders.push(`#${pathPart}`) 38 | } 39 | } 40 | placeholder = namePlaceholders.join('.') 41 | } else { 42 | // top level attribute 43 | const propertyMetadata = metadata && metadata.forProperty(attributePath) 44 | attributeNames[`#${attributePath}`] = propertyMetadata ? propertyMetadata.nameDb : attributePath 45 | placeholder = `#${attributePath}` 46 | } 47 | 48 | return { 49 | placeholder, 50 | attributeNames, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/is-function-operator.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { FUNCTION_OPERATORS } from '../function-operators.const' 5 | import { ConditionOperator } from '../type/condition-operator.type' 6 | import { FunctionOperator } from '../type/function-operator.type' 7 | 8 | /** 9 | * An operator can either be an comparator or a function, this method helps to check for function operator 10 | * @param {ConditionOperator} operator 11 | * @returns {boolean} Returns true if the operator is a function operator, false otherwise 12 | * @hidden 13 | */ 14 | export function isFunctionOperator(operator: ConditionOperator): operator is FunctionOperator { 15 | return (FUNCTION_OPERATORS).includes(operator) 16 | } 17 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/is-no-param-function-operator.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { NON_PARAM_FUNCTION_OPERATORS } from '../non-param-function-operators.const' 5 | import { FunctionOperator } from '../type/function-operator.type' 6 | 7 | /** 8 | * @returns {boolean} Returns true for all function operators with no param false otherwise 9 | * @hidden 10 | */ 11 | export function isNoParamFunctionOperator(operator: FunctionOperator): boolean { 12 | return NON_PARAM_FUNCTION_OPERATORS.includes(operator) 13 | } 14 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/operator-for-alias.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { OperatorAlias } from '../type/condition-operator-alias.type' 5 | import { OPERATOR_TO_ALIAS_MAP } from '../type/condition-operator-to-alias-map.const' 6 | import { ConditionOperator } from '../type/condition-operator.type' 7 | 8 | /** 9 | * @hidden 10 | */ 11 | export function operatorForAlias(alias: OperatorAlias): ConditionOperator | undefined { 12 | let operator: ConditionOperator | undefined 13 | Object.keys(OPERATOR_TO_ALIAS_MAP).forEach((key) => { 14 | const a = OPERATOR_TO_ALIAS_MAP[key] 15 | if (Array.isArray(a)) { 16 | if (a.includes(alias)) { 17 | operator = key 18 | } 19 | } else { 20 | if (a === alias) { 21 | operator = key 22 | } 23 | } 24 | }) 25 | 26 | return operator 27 | } 28 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/operator-parameter-arity.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ConditionOperator } from '../type/condition-operator.type' 5 | import { isFunctionOperator } from './is-function-operator.function' 6 | import { isNoParamFunctionOperator } from './is-no-param-function-operator.function' 7 | 8 | /** 9 | * Every expression condition operator has a predefined arity (amount) of function parameters, this method 10 | * returns this value 11 | * 12 | * @returns {number} The amount of required method parameters when calling an operator function 13 | * @hidden 14 | */ 15 | export function operatorParameterArity(operator: ConditionOperator): number { 16 | if (isFunctionOperator(operator) && isNoParamFunctionOperator(operator)) { 17 | return 0 18 | } else { 19 | switch (operator) { 20 | case '=': 21 | case '>': 22 | case '>=': 23 | case '<': 24 | case '<=': 25 | case '<>': 26 | case 'begins_with': 27 | case 'attribute_type': 28 | case 'contains': 29 | case 'not_contains': 30 | case 'IN': 31 | return 1 32 | case 'BETWEEN': 33 | return 2 34 | default: 35 | throw new Error(`no parameter arity defined for operator ${operator}`) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/resolve-attribute-value-name-conflicts.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Attributes } from '../../../mapper/type/attribute.type' 5 | import { ConditionalParams } from '../../operation-params.type' 6 | import { Expression } from '../type/expression.type' 7 | import { uniqueAttributeValueName } from './unique-attribute-value-name.function' 8 | 9 | /** 10 | * resolves name conflict when expression uses an attributeValueName that is already used in given *Input 11 | * @param expression 12 | * @param params 13 | * @return safe-to-use Expression 14 | * @hidden 15 | */ 16 | export function resolveAttributeValueNameConflicts(expression: Expression, params: ConditionalParams): Expression { 17 | let attributeValues: Attributes = {} 18 | let statement: string = expression.statement 19 | 20 | if (params.ExpressionAttributeValues) { 21 | const existingAttributeValueNames = Object.keys(params.ExpressionAttributeValues) 22 | Object.keys(expression.attributeValues) 23 | .map((key) => [key, uniqueAttributeValueName(key.replace(':', ''), existingAttributeValueNames)]) 24 | .forEach(([oldValName, newValName]) => { 25 | attributeValues[newValName] = expression.attributeValues[oldValName] 26 | // split-join based replaceAll 27 | statement = statement.split(oldValName).join(newValName) 28 | }) 29 | } else { 30 | attributeValues = expression.attributeValues 31 | } 32 | 33 | return { ...expression, attributeValues, statement } 34 | } 35 | -------------------------------------------------------------------------------- /src/dynamo/expression/functions/unique-attribute-value-name.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { attributeNameReplacer } from './attribute-name-replacer.function' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const BRACED_INDEX_REGEX = /\[(\d+)]/g 10 | 11 | /** 12 | * Creates a unique attribute value placeholder name to use in the expression 13 | * 14 | * @returns {string} The unique attribute value placeholder name in respect to the given existing value names (no duplicates allowed) 15 | * @hidden 16 | */ 17 | export function uniqueAttributeValueName(key: string, existingValueNames?: string[]): string { 18 | key = key.replace(/\./g, '__').replace(BRACED_INDEX_REGEX, attributeNameReplacer) 19 | let potentialName = `:${key}` 20 | let idx = 1 21 | 22 | if (existingValueNames && existingValueNames.length) { 23 | while (existingValueNames.includes(potentialName)) { 24 | idx++ 25 | potentialName = `:${key}_${idx}` 26 | } 27 | } 28 | 29 | return potentialName 30 | } 31 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/and.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function' 5 | import { mergeConditions } from './merge-conditions.function' 6 | 7 | /** 8 | * function to combine multiple conditions with 'and' 9 | * @example 10 | * ```typescript 11 | * and(attribute('propA').eq('foo'), attribute('propB').eq('bar')) 12 | * ``` 13 | */ 14 | export function and( 15 | ...conditionDefinitionFns: ConditionExpressionDefinitionFunction[] 16 | ): ConditionExpressionDefinitionFunction { 17 | return mergeConditions('AND', conditionDefinitionFns) 18 | } 19 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/attribute.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ModelConstructor } from '../../../model/model-constructor' 5 | import { propertyDefinitionFunction } from '../request-expression-builder' 6 | import { 7 | ConditionExpressionDefinitionChain, 8 | ConditionExpressionDefinitionChainTyped, 9 | } from '../type/condition-expression-definition-chain' 10 | 11 | /** 12 | * Use this method when accessing a top level attribute of a model with strict typing of the value in chained function 13 | * @example 14 | * ```typescript 15 | * 16 | * @Model() 17 | * class Person{ 18 | * 19 | * @PartitionKey() 20 | * id: string 21 | * age: number 22 | * } 23 | * 24 | * store 25 | * .scan() 26 | * .where(attribute2(Person, 'age').equals(5)) 27 | * .exec() 28 | * ``` 29 | * 30 | * When using the attribute2 we have type support for the equals (and all other condition functions) value, 31 | * it can only be number, because the type of age is number too, this only works when not using a custom mapper. 32 | * The downside of the strict typing is the model constructor parameter which is only required for typing reasons 33 | */ 34 | export function attribute2( 35 | modelConstructor: ModelConstructor, 36 | attributePath: K, 37 | ): ConditionExpressionDefinitionChainTyped { 38 | return propertyDefinitionFunction(attributePath) 39 | } 40 | 41 | /** 42 | * Use this method when accessing a top level attribute of a model to have type checking of the attributePath 43 | * @example 44 | * ```typescript 45 | * attribute('myProp').eq('foo') 46 | * ``` 47 | */ 48 | export function attribute(attributePath: keyof T): ConditionExpressionDefinitionChain 49 | 50 | /** 51 | * Use this method when accessing a nested attribute of a model 52 | */ 53 | export function attribute(attributePath: string): ConditionExpressionDefinitionChain 54 | 55 | export function attribute(attributePath: keyof T): ConditionExpressionDefinitionChain { 56 | return propertyDefinitionFunction(attributePath) 57 | } 58 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/merge-conditions.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { attribute } from './attribute.function' 2 | import { mergeConditions } from './merge-conditions.function' 3 | 4 | describe('mergeCondition statements', () => { 5 | test('no redundant parentheses for single condition', () => { 6 | const conditionDefinitionFns = [attribute('name').beginsWith('sample')] 7 | 8 | const conditions = mergeConditions('OR', conditionDefinitionFns) 9 | const expression = conditions(undefined, undefined) 10 | expect(expression.statement).toEqual('begins_with (#name, :name)') 11 | }) 12 | 13 | test('no redundant parentheses for multiple condition', () => { 14 | const conditionDefinitionFns = [attribute('name').beginsWith('sample'), attribute('fullName').beginsWith('sample')] 15 | 16 | const conditions = mergeConditions('OR', conditionDefinitionFns) 17 | const expression = conditions(undefined, undefined) 18 | 19 | expect(expression.statement).toEqual('(begins_with (#name, :name) OR begins_with (#fullName, :fullName))') 20 | }) 21 | 22 | test('no redundant parentheses for single condition combined', () => { 23 | const conditionDefinitionFns = [attribute('name').beginsWith('sample'), attribute('fullName').beginsWith('sample')] 24 | 25 | const conditions = mergeConditions('OR', conditionDefinitionFns) 26 | const andConditions = mergeConditions('AND', [conditions]) 27 | const expression = andConditions(undefined, undefined) 28 | expect(expression.statement).toEqual('(begins_with (#name, :name) OR begins_with (#fullName, :fullName))') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/not.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Metadata } from '../../../decorator/metadata/metadata' 5 | import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function' 6 | import { Expression } from '../type/expression.type' 7 | 8 | /** 9 | * function to negate a condition 10 | * @example 11 | * ```typescript 12 | * not(attribute('propA').eq('foo')) 13 | * ``` 14 | */ 15 | export function not( 16 | conditionDefinitionFn: ConditionExpressionDefinitionFunction, 17 | ): ConditionExpressionDefinitionFunction { 18 | return (expressionAttributeValues: string[] | undefined, metadata: Metadata | undefined): Expression => { 19 | const condition = conditionDefinitionFn(expressionAttributeValues, metadata) 20 | condition.statement = `NOT ${condition.statement}` 21 | return condition 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/or.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ConditionExpressionDefinitionFunction } from '../type/condition-expression-definition-function' 5 | import { mergeConditions } from './merge-conditions.function' 6 | 7 | /** 8 | * function to combine multiple conditions with or 9 | * @example 10 | * ```typescript 11 | * or(attribute('propA').eq('foo'), attribute('propB').eq('bar')) 12 | * ``` 13 | */ 14 | export function or( 15 | ...conditionDefinitionFns: ConditionExpressionDefinitionFunction[] 16 | ): ConditionExpressionDefinitionFunction { 17 | return mergeConditions('OR', conditionDefinitionFns) 18 | } 19 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/public.api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | export * from './and.function' 5 | export * from './attribute.function' 6 | export * from './not.function' 7 | export * from './or.function' 8 | export * from './update.function' 9 | -------------------------------------------------------------------------------- /src/dynamo/expression/logical-operator/update.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ModelConstructor } from '../../../model/model-constructor' 5 | import { updateDefinitionFunction } from '../request-expression-builder' 6 | import { 7 | UpdateExpressionDefinitionChain, 8 | UpdateExpressionDefinitionChainTyped, 9 | } from '../type/update-expression-definition-chain' 10 | 11 | /** 12 | * Use this method when accessing a top level attribute of a model with strict typing of the value in chained function 13 | * @example 14 | * ```typescript 15 | * @Model() 16 | * class Person { 17 | * 18 | * @PartitionKey() 19 | * id: string 20 | * age: number 21 | * } 22 | * 23 | * personStore.update('idValue') 24 | * .operations(update2(Person, 'age').set(5)) 25 | * .exec() 26 | * ``` 27 | * 28 | * When using the update2 we have type support for the set (and all other update functions) value, 29 | * it can only be number, because the type of age is number too, this only works when not using a custom mapper. 30 | * The downside of the strict typing is the model constructor parameter which is only required for typing reasons. 31 | */ 32 | export function update2( 33 | modelConstructor: ModelConstructor, 34 | attributePath: K, 35 | ): UpdateExpressionDefinitionChainTyped { 36 | return updateDefinitionFunction(attributePath) 37 | } 38 | 39 | /** 40 | * Use this method when accessing a top level attribute of a model to have type checking for attributePath 41 | * @example 42 | * ```typescript 43 | * update('myProp').set('foo') 44 | * ``` 45 | */ 46 | export function update(attributePath: keyof T): UpdateExpressionDefinitionChain 47 | 48 | /** 49 | * Use this method when accessing a nested attribute of a model 50 | */ 51 | export function update(attributePath: string): UpdateExpressionDefinitionChain 52 | 53 | export function update(attributePath: keyof T): UpdateExpressionDefinitionChain { 54 | return updateDefinitionFunction(attributePath) 55 | } 56 | -------------------------------------------------------------------------------- /src/dynamo/expression/non-param-function-operators.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { FunctionOperator } from './type/function-operator.type' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const NON_PARAM_FUNCTION_OPERATORS: FunctionOperator[] = ['attribute_exists', 'attribute_not_exists'] 10 | -------------------------------------------------------------------------------- /src/dynamo/expression/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | export * from './logical-operator/public.api' 5 | export * from './type/public-api' 6 | export * from './util' 7 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/comparator-operator.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type ComparatorOperator = '=' | '<>' | '<=' | '<' | '>=' | '>' 8 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/condition-expression-definition-chain.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ExtractListType } from '../../../helper/extract-list-type.type' 5 | import { AttributeType } from '../../../mapper/type/attribute-type.type' 6 | import { ConditionalParamsHost } from '../../operation-params.type' 7 | import { ConditionExpressionDefinitionFunction } from './condition-expression-definition-function' 8 | 9 | /** 10 | * see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html for full documentation 11 | */ 12 | interface ConditionFunctions { 13 | equals: (value: T) => R 14 | eq: (value: T) => R 15 | ne: (value: T) => R 16 | lte: (value: T) => R 17 | lt: (value: T) => R 18 | gte: (value: T) => R 19 | gt: (value: T) => R 20 | null: () => R 21 | notNull: () => R 22 | contains: (value: T | ExtractListType) => R 23 | notContains: (value: T | ExtractListType) => R 24 | type: (value: AttributeType) => R 25 | in: (value: T[]) => R 26 | beginsWith: (value: T) => R 27 | between: (value1: T, value2: T) => R 28 | attributeExists: () => R 29 | attributeNotExists: () => R 30 | } 31 | 32 | export type ConditionExpressionDefinitionChain = ConditionFunctions 33 | 34 | export type ConditionExpressionDefinitionChainTyped = ConditionFunctions< 35 | T[K], 36 | ConditionExpressionDefinitionFunction 37 | > 38 | 39 | export type RequestConditionFunctionTyped = ConditionFunctions< 40 | T[K], 41 | R 42 | > 43 | export type RequestConditionFunction = ConditionFunctions 44 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/condition-expression-definition-function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Metadata } from '../../../decorator/metadata/metadata' 5 | import { Expression } from './expression.type' 6 | 7 | /** 8 | * @hidden 9 | */ 10 | export type ConditionExpressionDefinitionFunction = ( 11 | expressionAttributeValues: string[] | undefined, 12 | metadata: Metadata | undefined, 13 | ) => Expression 14 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/condition-operator-alias.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type OperatorAlias = 8 | | 'equals' 9 | | 'eq' 10 | | 'ne' 11 | | 'lte' 12 | | 'lt' 13 | | 'gte' 14 | | 'gt' 15 | | 'null' 16 | | 'notNull' 17 | | 'type' 18 | | 'beginsWith' 19 | | 'contains' 20 | | 'notContains' 21 | | 'in' 22 | | 'between' 23 | | 'attributeExists' 24 | | 'attributeNotExists' 25 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/condition-operator-to-alias-map.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { OperatorAlias } from './condition-operator-alias.type' 5 | import { ConditionOperator } from './condition-operator.type' 6 | 7 | /** 8 | * mapped type 9 | * @hidden 10 | */ 11 | export interface AliasedOperatorMapEntry extends Record { 12 | // index signature 13 | [key: string]: OperatorAlias | OperatorAlias[] 14 | } 15 | 16 | /** 17 | * @hidden 18 | */ 19 | export const OPERATOR_TO_ALIAS_MAP: AliasedOperatorMapEntry = { 20 | '=': ['equals', 'eq'], 21 | '<>': 'ne', 22 | '<=': 'lte', 23 | '<': 'lt', 24 | '>=': 'gte', 25 | '>': 'gt', 26 | attribute_not_exists: ['attributeNotExists', 'null'], 27 | attribute_exists: ['attributeExists', 'notNull'], 28 | attribute_type: 'type', 29 | contains: 'contains', 30 | not_contains: 'notContains', 31 | IN: 'in', 32 | begins_with: 'beginsWith', 33 | BETWEEN: 'between', 34 | } 35 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/condition-operator.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ComparatorOperator } from './comparator-operator.type' 5 | import { FunctionOperator } from './function-operator.type' 6 | 7 | /** 8 | * 9 | * http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax 10 | * 11 | * condition-expression ::= 12 | * operand comparator operand 13 | * | operand BETWEEN operand AND operand 14 | * | operand IN ( operand (',' operand (, ...) )) 15 | * | function 16 | * | condition AND condition 17 | * | condition OR condition 18 | * | NOT condition 19 | * | ( condition ) 20 | * 21 | * comparator ::= 22 | * = 23 | * | <> 24 | * | < 25 | * | <= 26 | * | > 27 | * | >= 28 | * 29 | * function ::= 30 | * attribute_exists (path) 31 | * | attribute_not_exists (path) 32 | * | attribute_type (path, type) 33 | * | begins_with (path, substr) 34 | * | contains (path, operand) 35 | * | size (path) 36 | * @hidden 37 | */ 38 | export type ConditionOperator = FunctionOperator | ComparatorOperator 39 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/expression-type.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type ExpressionType = 'ConditionExpression' | 'FilterExpression' | 'KeyConditionExpression' 8 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/expression.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Attributes } from '../../../mapper/type/attribute.type' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export interface Expression { 10 | attributeNames: Record 11 | attributeValues: Attributes 12 | statement: string 13 | } 14 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/function-operator.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type FunctionOperator = 8 | | 'attribute_exists' 9 | | 'attribute_not_exists' 10 | | 'attribute_type' 11 | | 'begins_with' 12 | | 'contains' 13 | | 'not_contains' 14 | | 'IN' 15 | | 'BETWEEN' 16 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | export * from './comparator-operator.type' 5 | export * from './condition-expression-definition-chain' 6 | export * from './condition-expression-definition-function' 7 | export * from './condition-operator.type' 8 | export * from './condition-operator-alias.type' 9 | export * from './condition-operator-to-alias-map.const' 10 | export * from './expression.type' 11 | export * from './expression-type.type' 12 | export * from './function-operator.type' 13 | export * from './sort-key-condition-function' 14 | export * from './update-action.type' 15 | export * from './update-action-def' 16 | export * from './update-action-defs.const' 17 | export * from './update-expression.type' 18 | export * from './update-expression-definition-chain' 19 | export * from './update-expression-definition-function' 20 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/sort-key-condition-function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { ConditionalParamsHost } from '../../operation-params.type' 5 | 6 | /* 7 | * Valid comparisons for the sort key condition are as follows: 8 | * sortKeyName = :sortkeyval - true if the sort key value is equal to :sortkeyval. 9 | * sortKeyName < :sortkeyval - true if the sort key value is less than :sortkeyval. 10 | * sortKeyName <= :sortkeyval - true if the sort key value is less than or equal to :sortkeyval. 11 | * sortKeyName > :sortkeyval - true if the sort key value is greater than :sortkeyval. 12 | * sortKeyName >= :sortkeyval - true if the sort key value is greater than or equal to :sortkeyval. 13 | * sortKeyName BETWEEN :sortkeyval1 AND :sortkeyval2 - true if the sort key value is greater than or equal to :sortkeyval1, and less than or equal to :sortkeyval2. 14 | * begins_with ( sortKeyName, :sortkeyval ) - true if the sort key value begins with a particular operand. 15 | * (You cannot use this function with a sort key that is of formType Number.) Note that the function name begins_with is case-sensitive. 16 | */ 17 | export interface SortKeyConditionFunction { 18 | equals: (value: any) => R 19 | eq: (value: any) => R 20 | lt: (value: any) => R 21 | lte: (value: any) => R 22 | gt: (value: any) => R 23 | gte: (value: any) => R 24 | between: (value1: any, value2: any) => R 25 | beginsWith: (value: any) => R 26 | } 27 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-action-def.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { UpdateActionKeyword } from './update-action-keyword.type' 5 | import { UpdateAction } from './update-action.type' 6 | 7 | export class UpdateActionDef { 8 | constructor(public actionKeyword: UpdateActionKeyword, public action: UpdateAction) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-action-defs.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { UpdateActionDef } from './update-action-def' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const UPDATE_ACTION_DEFS: UpdateActionDef[] = [ 10 | // SET 11 | new UpdateActionDef('SET', 'incrementBy'), 12 | new UpdateActionDef('SET', 'decrementBy'), 13 | new UpdateActionDef('SET', 'set'), 14 | new UpdateActionDef('SET', 'appendToList'), 15 | // REMOVE 16 | new UpdateActionDef('REMOVE', 'remove'), 17 | new UpdateActionDef('REMOVE', 'removeFromListAt'), 18 | // ADD 19 | new UpdateActionDef('ADD', 'add'), 20 | // DELETE 21 | new UpdateActionDef('DELETE', 'removeFromSet'), 22 | ] 23 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-action-keyword.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export type UpdateActionKeyword = 'SET' | 'REMOVE' | 'ADD' | 'DELETE' 8 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-action.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | /** 5 | * 6 | * update expressions support these 4 base operations: 7 | * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html 8 | * 9 | * update-expression ::= 10 | * [ SET action [, action] ... ] 11 | * [ REMOVE action [, action] ...] 12 | * [ ADD action [, action] ... ] 13 | * [ DELETE action [, action] ...] 14 | * 15 | * we provide our own aliases for easier usage 16 | * @hidden 17 | */ 18 | export type UpdateAction = 19 | | 'incrementBy' 20 | | 'decrementBy' 21 | | 'set' 22 | | 'setAt' 23 | | 'appendToList' 24 | | 'remove' 25 | | 'removeFromListAt' 26 | | 'add' 27 | | 'removeFromSet' 28 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-expression-definition-function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Metadata } from '../../../decorator/metadata/metadata' 5 | import { UpdateExpression } from './update-expression.type' 6 | 7 | /** 8 | * @hidden 9 | */ 10 | export type UpdateExpressionDefinitionFunction = ( 11 | expressionAttributeValues: string[] | undefined, 12 | metadata: Metadata | undefined, 13 | ) => UpdateExpression 14 | -------------------------------------------------------------------------------- /src/dynamo/expression/type/update-expression.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { Expression } from './expression.type' 5 | import { UpdateActionKeyword } from './update-action-keyword.type' 6 | 7 | /** 8 | * @hidden 9 | */ 10 | export interface UpdateExpression extends Expression { 11 | type: UpdateActionKeyword 12 | } 13 | -------------------------------------------------------------------------------- /src/dynamo/expression/update-action-keywords.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | import { UpdateActionKeyword } from './type/update-action-keyword.type' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export const UPDATE_ACTION_KEYWORDS: UpdateActionKeyword[] = ['SET', 'REMOVE', 'ADD', 'DELETE'] 10 | -------------------------------------------------------------------------------- /src/dynamo/expression/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { dynamicTemplate } from './util' 2 | 3 | describe('util', () => { 4 | it('should replace template vars dynamically', () => { 5 | // tslint:disable-next-line 6 | const error = 'my sample error ${errorMessage} with some stuff in there and another ${secondValue}' 7 | const built = dynamicTemplate(error, { errorMessage: 'the message', secondValue: 5 }) 8 | expect(built).toBe('my sample error the message with some stuff in there and another 5') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/dynamo/expression/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module expression 3 | */ 4 | 5 | /** 6 | * @hidden 7 | */ 8 | export function dynamicTemplate(templateString: string, templateVariables: Record) { 9 | const keys = Object.keys(templateVariables) 10 | const values = Object.values(templateVariables) 11 | // tslint:disable-next-line:function-constructor 12 | const templateFunction = new Function(...keys, `return \`${templateString}\`;`) 13 | return templateFunction(...values) 14 | } 15 | -------------------------------------------------------------------------------- /src/dynamo/get-table-name.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { resetDynamoEasyConfig } from '../../test/helper/resetDynamoEasyConfig.function' 2 | import { Organization, SimpleModel } from '../../test/models' 3 | import { updateDynamoEasyConfig } from '../config/update-config.function' 4 | import { metadataForModel } from '../decorator/metadata/metadata-for-model.function' 5 | import { getTableName } from './get-table-name.function' 6 | 7 | describe('getTableName', () => { 8 | afterEach(resetDynamoEasyConfig) 9 | 10 | it('correct table name - default by class', () => { 11 | expect(getTableName(SimpleModel)).toBe('simple-models') 12 | }) 13 | 14 | it('correct table name - default by metaData', () => { 15 | expect(getTableName(metadataForModel(SimpleModel))).toBe('simple-models') 16 | }) 17 | 18 | it('correct table name - by decorator', () => { 19 | expect(getTableName(Organization)).toBe('Organization') 20 | }) 21 | 22 | it('correct table name - by tableNameResolver', () => { 23 | updateDynamoEasyConfig({ tableNameResolver: (tableName) => `${tableName}-with-special-thing` }) 24 | expect(getTableName(SimpleModel)).toBe('simple-models-with-special-thing') 25 | expect(getTableName(metadataForModel(Organization))).toBe('Organization-with-special-thing') 26 | }) 27 | 28 | it('throw error because table name is invalid', () => { 29 | // tslint:disable-next-line:no-unused-expression 30 | updateDynamoEasyConfig({ tableNameResolver: (tableName) => `${tableName}$` }) 31 | expect(() => getTableName(metadataForModel(SimpleModel))).toThrowError() 32 | expect(() => getTableName(Organization)).toThrowError() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/dynamo/get-table-name.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import { dynamoEasyConfig } from '../config/dynamo-easy-config' 5 | import { Metadata } from '../decorator/metadata/metadata' 6 | import { metadataForModel } from '../decorator/metadata/metadata-for-model.function' 7 | import { ModelConstructor } from '../model/model-constructor' 8 | 9 | /** 10 | * only contains these characters «a-z A-Z 0-9 - _ .» and is between 3 and 255 characters long 11 | * http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-naming-rules 12 | * @hidden 13 | */ 14 | const REGEX_TABLE_NAME = /^[a-zA-Z0-9_\-.]{3,255}$/ 15 | 16 | /** 17 | * @hidden 18 | */ 19 | export function getTableName(metaDataOrModelClazz: Metadata | ModelConstructor): string { 20 | const modelOptions = 21 | metaDataOrModelClazz instanceof Metadata 22 | ? metaDataOrModelClazz.modelOptions 23 | : metadataForModel(metaDataOrModelClazz).modelOptions 24 | 25 | const tableName = dynamoEasyConfig.tableNameResolver(modelOptions.tableName) 26 | 27 | if (!REGEX_TABLE_NAME.test(tableName)) { 28 | throw new Error( 29 | `make sure the table name «${tableName}» is valid (see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-naming-rules for details)`, 30 | ) 31 | } 32 | return tableName 33 | } 34 | -------------------------------------------------------------------------------- /src/dynamo/operation-params.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export interface ConditionalParamsHost { 10 | readonly params: ConditionalParams 11 | } 12 | 13 | /** 14 | * @hidden 15 | */ 16 | export interface ConditionalParams { 17 | expressionAttributeNames?: DynamoDB.ExpressionAttributeNameMap 18 | expressionAttributeValues?: DynamoDB.ExpressionAttributeValueMap 19 | [key: string]: any 20 | } 21 | 22 | /** 23 | * @hidden 24 | */ 25 | export interface UpdateParamsHost { 26 | readonly params: DynamoDB.UpdateItemInput | DynamoDB.Update 27 | } 28 | -------------------------------------------------------------------------------- /src/dynamo/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | export * from './batchget/public-api' 5 | export * from './batchwrite/public-api' 6 | export * from './dynamo-db-wrapper' 7 | export * from './dynamo-store' 8 | export * from './expression/public-api' 9 | export * from './request/public-api' 10 | export * from './session-validity-ensurer.type' 11 | export * from './table-name-resolver.type' 12 | export * from './transactget/public-api' 13 | export * from './transactwrite/public-api' 14 | -------------------------------------------------------------------------------- /src/dynamo/request/batchgetsingletable/batch-get-single-table.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | /** 7 | * Response from {@link BatchGetSingleTableRequest}::exec 8 | */ 9 | export interface BatchGetSingleTableResponse { 10 | /** 11 | * A map of table name to a list of items. Each object in Responses consists of a table name, along with a map of attribute data consisting of the data type and attribute value, as specified by ProjectionExpression. 12 | */ 13 | Items: T[] 14 | /** 15 | * A map of tables and their respective keys that were not processed with the current response. The UnprocessedKeys value is in the same form as RequestItems, so the value can be provided directly to a subsequent BatchGetItem operation. For more information, see RequestItems in the Request Parameters section. Each element consists of: Keys - An array of primary key attribute values that define specific items in the table. ProjectionExpression - One or more attributes to be retrieved from the table or index. By default, all attributes are returned. If a requested attribute is not found, it does not appear in the result. ConsistentRead - The consistency of a read operation. If set to true, then a strongly consistent read is used; otherwise, an eventually consistent read is used. If there are no unprocessed keys remaining, the response contains an empty UnprocessedKeys map. 16 | */ 17 | UnprocessedKeys?: DynamoDB.BatchGetRequestMap 18 | /** 19 | * The read capacity units consumed by the entire BatchGetItem operation. Each element consists of: TableName - The table that consumed the provisioned throughput. CapacityUnits - The total number of capacity units consumed. 20 | */ 21 | ConsumedCapacity?: DynamoDB.ConsumedCapacityMultiple 22 | } 23 | -------------------------------------------------------------------------------- /src/dynamo/request/class-diagram.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiftcode/dynamo-easy/2e1ad7c0bc41eeb8a45ba873d17e9d998fdbf41c/src/dynamo/request/class-diagram.monopic -------------------------------------------------------------------------------- /src/dynamo/request/delete/delete.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createLogger, Logger } from '../../../logger/logger' 6 | import { createKeyAttributes } from '../../../mapper/mapper' 7 | import { ModelConstructor } from '../../../model/model-constructor' 8 | import { DynamoDbWrapper } from '../../dynamo-db-wrapper' 9 | import { WriteRequest } from '../write.request' 10 | 11 | /** 12 | * Request class for the DeleteItem operation. 13 | */ 14 | export class DeleteRequest extends WriteRequest< 15 | T, 16 | T2, 17 | DynamoDB.DeleteItemInput, 18 | DynamoDB.DeleteItemOutput, 19 | DeleteRequest 20 | > { 21 | protected readonly logger: Logger 22 | 23 | constructor(dynamoDBWrapper: DynamoDbWrapper, modelClazz: ModelConstructor, partitionKey: any, sortKey?: any) { 24 | super(dynamoDBWrapper, modelClazz) 25 | this.logger = createLogger('dynamo.request.DeleteRequest', modelClazz) 26 | this.params.Key = createKeyAttributes(this.metadata, partitionKey, sortKey) 27 | } 28 | 29 | returnValues(returnValues: 'ALL_OLD'): DeleteRequest 30 | returnValues(returnValues: 'NONE'): DeleteRequest 31 | returnValues(returnValues: 'ALL_OLD' | 'NONE'): DeleteRequest { 32 | this.params.ReturnValues = returnValues 33 | return this 34 | } 35 | 36 | protected doRequest(params: DynamoDB.DeleteItemInput): Promise { 37 | return this.dynamoDBWrapper.deleteItem(params) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/dynamo/request/delete/delete.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | export interface DeleteResponse { 7 | ConsumedCapacity?: DynamoDB.ConsumedCapacity 8 | ItemCollectionMetrics?: DynamoDB.ItemCollectionMetrics 9 | Item: T 10 | } 11 | -------------------------------------------------------------------------------- /src/dynamo/request/get/get.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | /** 7 | * copied from aws-sdk/clients/dynamoDb GetItemOutput but added generics, because we process the items and map them 8 | * to an actual type 9 | */ 10 | export interface GetResponse { 11 | /** 12 | * A map of attribute names to AttributeValue objects (subset if ProjectionExpression was defined). 13 | */ 14 | Item: T | null 15 | /** 16 | * The capacity units consumed by the GetItem operation. The data returned includes the total provisioned throughput consumed, along with statistics for the table and any indexes involved in the operation. ConsumedCapacity is only returned if the ReturnConsumedCapacity parameter was specified. For more information, see Provisioned Throughput in the Amazon DynamoDB Developer Guide. 17 | */ 18 | ConsumedCapacity?: DynamoDB.ConsumedCapacity 19 | } 20 | -------------------------------------------------------------------------------- /src/dynamo/request/helper/add-projection-expression-param.function.spec.ts: -------------------------------------------------------------------------------- 1 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 2 | import { ComplexModel, SimpleWithPartitionKeyModel } from '../../../../test/models' 3 | import { metadataForModel } from '../../../decorator/metadata/metadata-for-model.function' 4 | import { addProjectionExpressionParam } from './add-projection-expression-param.function' 5 | 6 | describe('add projection expression param function', () => { 7 | let params: DynamoDB.KeysAndAttributes 8 | 9 | beforeEach(() => { 10 | params = {} 11 | }) 12 | 13 | it('add single projection attribute to params', () => { 14 | addProjectionExpressionParam(['age'], params) 15 | expect(params.ProjectionExpression).toBeDefined() 16 | expect(params.ProjectionExpression).toBe('#age') 17 | expect(params.ExpressionAttributeNames).toEqual({ '#age': 'age' }) 18 | }) 19 | 20 | it('add multiple projection attribute to params', () => { 21 | addProjectionExpressionParam(['age', 'id'], params) 22 | expect(params.ProjectionExpression).toBeDefined() 23 | expect(params.ProjectionExpression).toBe('#age, #id') 24 | expect(params.ExpressionAttributeNames).toEqual({ '#age': 'age', '#id': 'id' }) 25 | }) 26 | 27 | it('add multiple projection attribute respecting given metadata to params', () => { 28 | addProjectionExpressionParam(['active', 'simpleProperty'], params, metadataForModel(ComplexModel)) 29 | expect(params.ProjectionExpression).toBeDefined() 30 | expect(params.ProjectionExpression).toBe('#active, #simpleProperty') 31 | expect(params.ExpressionAttributeNames).toEqual({ '#active': 'isActive', '#simpleProperty': 'simpleProperty' }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/dynamo/request/helper/add-projection-expression-param.function.ts: -------------------------------------------------------------------------------- 1 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 2 | import { Metadata } from '../../../decorator/metadata/metadata' 3 | import { resolveAttributeNames } from '../../expression/functions/attribute-names.function' 4 | 5 | /** 6 | * Adds ProjectionExpression param and expressionAttributeNames to the params object 7 | */ 8 | export function addProjectionExpressionParam( 9 | attributesToGet: Array, 10 | params: DynamoDB.QueryInput | DynamoDB.ScanInput | DynamoDB.GetItemInput | DynamoDB.KeysAndAttributes, 11 | metadata?: Metadata, 12 | ): void { 13 | const resolved = attributesToGet.map((attributeToGet) => resolveAttributeNames(attributeToGet, metadata)) 14 | params.ProjectionExpression = resolved.map((attr) => attr.placeholder).join(', ') 15 | resolved.forEach((r) => { 16 | params.ExpressionAttributeNames = { ...params.ExpressionAttributeNames, ...r.attributeNames } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/dynamo/request/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | export * from './delete/delete.request' 5 | export * from './delete/delete.response' 6 | export * from './get/get.request' 7 | export * from './get/get.response' 8 | export * from './put/put.request' 9 | export * from './put/put.response' 10 | export * from './query/query.request' 11 | export * from './query/query.response' 12 | export * from './scan/scan.request' 13 | export * from './scan/scan.response' 14 | export * from './update/update.request' 15 | export * from './update/update.response' 16 | export * from './batchgetsingletable/batch-get-single-table.request' 17 | export * from './batchgetsingletable/batch-get-single-table.response' 18 | export * from './read-many.request' 19 | export * from './transactgetsingletable/transact-get-single-table.request' 20 | export * from './transactgetsingletable/transact-get-single-table.response' 21 | -------------------------------------------------------------------------------- /src/dynamo/request/put/put.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createLogger, Logger } from '../../../logger/logger' 6 | import { toDb } from '../../../mapper/mapper' 7 | import { ModelConstructor } from '../../../model/model-constructor' 8 | import { DynamoDbWrapper } from '../../dynamo-db-wrapper' 9 | import { createIfNotExistsCondition } from '../../expression/create-if-not-exists-condition.function' 10 | import { WriteRequest } from '../write.request' 11 | 12 | /** 13 | * Request class for the PutItem operation. 14 | */ 15 | export class PutRequest extends WriteRequest< 16 | T, 17 | T2, 18 | DynamoDB.PutItemInput, 19 | DynamoDB.PutItemOutput, 20 | PutRequest 21 | > { 22 | protected readonly logger: Logger 23 | 24 | constructor(dynamoDBWrapper: DynamoDbWrapper, modelClazz: ModelConstructor, item: T) { 25 | super(dynamoDBWrapper, modelClazz) 26 | this.logger = createLogger('dynamo.request.PutRequest', modelClazz) 27 | this.params.Item = toDb(item, this.modelClazz) 28 | } 29 | 30 | /** 31 | * Adds a condition expression to the request, which makes sure the item will only be saved if the id does not exist 32 | * @param predicate if false is provided nothing happens (it does NOT remove the condition) 33 | */ 34 | ifNotExists(predicate: boolean = true): this { 35 | if (predicate) { 36 | this.onlyIf(...createIfNotExistsCondition(this.metadata)) 37 | } 38 | 39 | return this 40 | } 41 | 42 | returnValues(returnValues: 'ALL_OLD'): PutRequest 43 | returnValues(returnValues: 'NONE'): PutRequest 44 | returnValues(returnValues: 'ALL_OLD' | 'NONE'): PutRequest { 45 | this.params.ReturnValues = returnValues 46 | return this 47 | } 48 | 49 | protected doRequest(params: DynamoDB.PutItemInput): Promise { 50 | return this.dynamoDBWrapper.putItem(params) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/dynamo/request/put/put.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | export interface PutResponse { 7 | ConsumedCapacity?: DynamoDB.ConsumedCapacity 8 | ItemCollectionMetrics?: DynamoDB.ItemCollectionMetrics 9 | Item: T 10 | } 11 | -------------------------------------------------------------------------------- /src/dynamo/request/scan/scan.request.spec.ts: -------------------------------------------------------------------------------- 1 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 2 | import { ComplexModel } from '../../../../test/models' 3 | import { DynamoDbWrapper } from '../../dynamo-db-wrapper' 4 | import { ReadManyRequest } from '../read-many.request' 5 | import { ScanRequest } from './scan.request' 6 | 7 | describe('scan request', () => { 8 | let request: MyScanRequest 9 | let scanSpy: jasmine.Spy 10 | 11 | class MyScanRequest extends ScanRequest { 12 | constructor(dynamoDBWrapper: DynamoDbWrapper) { 13 | super(dynamoDBWrapper, ComplexModel) 14 | } 15 | 16 | get theLogger() { 17 | return this.logger 18 | } 19 | } 20 | 21 | beforeEach(() => { 22 | scanSpy = jasmine.createSpy().and.returnValue(Promise.resolve({ Count: 1 })) 23 | request = new MyScanRequest({ scan: scanSpy }) 24 | }) 25 | 26 | it('extends ReadManyRequest', () => { 27 | expect(request instanceof ReadManyRequest).toBeTruthy() 28 | }) 29 | 30 | it('default params', () => { 31 | expect(request.params).toEqual({ TableName: 'complex_model' }) 32 | }) 33 | 34 | it('execSingle', async () => { 35 | await request.execSingle() 36 | expect(scanSpy).toHaveBeenCalled() 37 | expect(scanSpy.calls.mostRecent().args[0]).toBeDefined() 38 | expect(scanSpy.calls.mostRecent().args[0].Limit).toBe(1) 39 | }) 40 | 41 | it('constructor creates logger', () => { 42 | expect(request.theLogger).toBeDefined() 43 | }) 44 | 45 | it('doRequest uses dynamoDBWrapper.scan', async () => { 46 | await request.exec() 47 | expect(scanSpy).toHaveBeenCalled() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/dynamo/request/scan/scan.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createLogger, Logger } from '../../../logger/logger' 6 | import { ModelConstructor } from '../../../model/model-constructor' 7 | import { DynamoDbWrapper } from '../../dynamo-db-wrapper' 8 | import { ReadManyRequest } from '../read-many.request' 9 | import { ScanResponse } from './scan.response' 10 | 11 | /** 12 | * Request class for the Scan operation. 13 | */ 14 | export class ScanRequest extends ReadManyRequest< 15 | T, 16 | T2, 17 | DynamoDB.ScanInput, 18 | DynamoDB.ScanOutput, 19 | ScanResponse, 20 | ScanRequest, 21 | ScanRequest> 22 | > { 23 | protected readonly logger: Logger 24 | 25 | constructor(dynamoDBWrapper: DynamoDbWrapper, modelClazz: ModelConstructor) { 26 | super(dynamoDBWrapper, modelClazz) 27 | this.logger = createLogger('dynamo.request.ScanRequest', modelClazz) 28 | } 29 | 30 | protected doRequest(params: DynamoDB.ScanInput): Promise { 31 | return this.dynamoDBWrapper.scan(params) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/dynamo/request/scan/scan.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | export interface ScanResponse { 7 | /** 8 | * An array of item attributes that match the scan criteria. Each element in this array consists of an attribute name and the value for that attribute (subset of attributes if ProjectionExpression was defined). 9 | */ 10 | Items: T[] 11 | /** 12 | * The number of items in the response. If you set ScanFilter in the request, then Count is the number of items returned after the filter was applied, and ScannedCount is the number of matching items before the filter was applied. If you did not use a filter in the request, then Count is the same as ScannedCount. 13 | */ 14 | Count: DynamoDB.Integer 15 | /** 16 | * The number of items evaluated, before any ScanFilter is applied. A high ScannedCount value with few, or no, Count results indicates an inefficient Scan operation. For more information, see Count and ScannedCount in the Amazon DynamoDB Developer Guide. If you did not use a filter in the request, then ScannedCount is the same as Count. 17 | */ 18 | ScannedCount?: DynamoDB.Integer 19 | /** 20 | * The primary key of the item where the operation stopped, inclusive of the previous result set. Use this value to start a new operation, excluding this value in the new request. If LastEvaluatedKey is empty, then the "last page" of results has been processed and there is no more data to be retrieved. If LastEvaluatedKey is not empty, it does not necessarily mean that there is more data in the result set. The only way to know when you have reached the end of the result set is when LastEvaluatedKey is empty. 21 | */ 22 | LastEvaluatedKey?: DynamoDB.Key 23 | /** 24 | * The capacity units consumed by the Scan operation. The data returned includes the total provisioned throughput consumed, along with statistics for the table and any indexes involved in the operation. ConsumedCapacity is only returned if the ReturnConsumedCapacity parameter was specified. For more information, see Provisioned Throughput in the Amazon DynamoDB Developer Guide. 25 | */ 26 | ConsumedCapacity?: DynamoDB.ConsumedCapacity 27 | } 28 | -------------------------------------------------------------------------------- /src/dynamo/request/standard.request.spec.ts: -------------------------------------------------------------------------------- 1 | import { Organization } from '../../../test/models' 2 | import { ModelConstructor } from '../../model/model-constructor' 3 | import { getTableName } from '../get-table-name.function' 4 | import { StandardRequest } from './standard.request' 5 | 6 | describe('StandardRequest', () => { 7 | class MyStandardRequest extends StandardRequest> { 8 | constructor(c: ModelConstructor) { 9 | super(null, c) 10 | } 11 | 12 | exec() { 13 | return Promise.resolve([]) 14 | } 15 | 16 | execFullResponse() { 17 | return Promise.resolve({}) 18 | } 19 | } 20 | 21 | it('creates default params with table name', () => { 22 | const msr = new MyStandardRequest(Organization) 23 | expect(msr.params).toEqual({ TableName: getTableName(Organization) }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/dynamo/request/standard.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { ModelConstructor } from '../../model/model-constructor' 6 | import { DynamoDbWrapper } from '../dynamo-db-wrapper' 7 | import { getTableName } from '../get-table-name.function' 8 | import { BaseRequest } from './base.request' 9 | 10 | /** 11 | * abstract class for all requests types that operate on exactly one dynamo table. 12 | * basically just sets the TableName info on input params. 13 | */ 14 | export abstract class StandardRequest< 15 | T, 16 | T2, 17 | I extends 18 | | DynamoDB.DeleteItemInput 19 | | DynamoDB.GetItemInput 20 | | DynamoDB.PutItemInput 21 | | DynamoDB.UpdateItemInput 22 | | DynamoDB.QueryInput 23 | | DynamoDB.ScanInput, 24 | R extends StandardRequest 25 | > extends BaseRequest { 26 | protected constructor(dynamoDBWrapper: DynamoDbWrapper, modelClazz: ModelConstructor) { 27 | super(dynamoDBWrapper, modelClazz) 28 | this.params.TableName = getTableName(this.metadata) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/dynamo/request/transactgetsingletable/transact-get-single-table.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { Omit } from '../../../model/omit.type' 6 | 7 | export type TransactGetResponse = Omit & { Items: T[] } 8 | -------------------------------------------------------------------------------- /src/dynamo/request/update/update.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module store-requests 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | export interface UpdateResponse { 7 | ConsumedCapacity?: DynamoDB.ConsumedCapacity 8 | ItemCollectionMetrics?: DynamoDB.ItemCollectionMetrics 9 | Item: T 10 | } 11 | -------------------------------------------------------------------------------- /src/dynamo/session-validity-ensurer.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | /** 5 | * Type for the session validity ensurer 6 | */ 7 | export type SessionValidityEnsurer = () => Promise 8 | -------------------------------------------------------------------------------- /src/dynamo/table-name-resolver.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | /** 5 | * Type for the table name resolver 6 | */ 7 | export type TableNameResolver = (tableName: string) => string 8 | -------------------------------------------------------------------------------- /src/dynamo/transactget/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-get 3 | */ 4 | export * from './transact-get.request' 5 | export * from './transact-get.request.type' 6 | export * from './transact-get-full.response' 7 | -------------------------------------------------------------------------------- /src/dynamo/transactget/transact-get-full.response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-get 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | 6 | export interface TransactGetFullResponse { 7 | Items: X 8 | ConsumedCapacity?: DynamoDB.ConsumedCapacityMultiple 9 | } 10 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | export * from './transact-condition-check' 5 | export * from './transact-delete' 6 | export * from './transact-put' 7 | export * from './transact-update' 8 | 9 | export * from './transact-write.request' 10 | export * from './transact-operation.type' 11 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-condition-check.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateModel } from '../../../test/models' 2 | import { Metadata } from '../../decorator/metadata/metadata' 3 | import { metadataForModel } from '../../decorator/metadata/metadata-for-model.function' 4 | import { createKeyAttributes } from '../../mapper/mapper' 5 | import { getTableName } from '../get-table-name.function' 6 | import { TransactConditionCheck } from './transact-condition-check' 7 | 8 | describe('TransactConditionCheck', () => { 9 | let op: TransactConditionCheck 10 | let metadata: Metadata 11 | beforeEach(() => { 12 | op = new TransactConditionCheck(UpdateModel, 'myId') 13 | metadata = metadataForModel(UpdateModel) 14 | }) 15 | 16 | it('correct transactItem', () => { 17 | op.onlyIfAttribute('name').eq('Foo Bar') 18 | expect(op.transactItem).toEqual({ 19 | ConditionCheck: { 20 | TableName: getTableName(UpdateModel), 21 | Key: createKeyAttributes(metadata, 'myId'), 22 | ConditionExpression: '#name = :name', 23 | ExpressionAttributeNames: { '#name': 'name' }, 24 | ExpressionAttributeValues: { ':name': { S: 'Foo Bar' } }, 25 | }, 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-condition-check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createKeyAttributes } from '../../mapper/mapper' 6 | import { ModelConstructor } from '../../model/model-constructor' 7 | import { TransactBaseOperation } from './transact-base-operation' 8 | 9 | /** 10 | * TransactOperation class for transactional condition checks. 11 | */ 12 | export class TransactConditionCheck extends TransactBaseOperation< 13 | T, 14 | DynamoDB.ConditionCheck, 15 | TransactConditionCheck 16 | > { 17 | constructor(modelClazz: ModelConstructor, partitionKey: any, sortKey?: any) { 18 | super(modelClazz) 19 | this.params.Key = createKeyAttributes(this.metadata, partitionKey, sortKey) 20 | } 21 | 22 | get transactItem() { 23 | return { 24 | ConditionCheck: { ...this.params }, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateModel } from '../../../test/models' 2 | import { Metadata } from '../../decorator/metadata/metadata' 3 | import { metadataForModel } from '../../decorator/metadata/metadata-for-model.function' 4 | import { createKeyAttributes } from '../../mapper/mapper' 5 | import { getTableName } from '../get-table-name.function' 6 | import { TransactDelete } from './transact-delete' 7 | 8 | describe('TransactDelete', () => { 9 | let op: TransactDelete 10 | let metadata: Metadata 11 | beforeEach(() => { 12 | op = new TransactDelete(UpdateModel, 'myId') 13 | metadata = metadataForModel(UpdateModel) 14 | }) 15 | 16 | it('correct transactItem', () => { 17 | op.onlyIfAttribute('name').eq('Foo Bar') 18 | expect(op.transactItem).toEqual({ 19 | Delete: { 20 | TableName: getTableName(UpdateModel), 21 | Key: createKeyAttributes(metadata, 'myId'), 22 | ConditionExpression: '#name = :name', 23 | ExpressionAttributeNames: { '#name': 'name' }, 24 | ExpressionAttributeValues: { ':name': { S: 'Foo Bar' } }, 25 | }, 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-delete.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createKeyAttributes } from '../../mapper/mapper' 6 | import { ModelConstructor } from '../../model/model-constructor' 7 | import { TransactBaseOperation } from './transact-base-operation' 8 | 9 | /** 10 | * TransactOperation class for transactional delete items 11 | */ 12 | export class TransactDelete extends TransactBaseOperation> { 13 | constructor(modelClazz: ModelConstructor, partitionKey: any, sortKey?: any) { 14 | super(modelClazz) 15 | this.params.Key = createKeyAttributes(this.metadata, partitionKey, sortKey) 16 | } 17 | 18 | get transactItem() { 19 | return { 20 | Delete: { ...this.params }, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-operation.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | import { TransactConditionCheck } from './transact-condition-check' 5 | import { TransactDelete } from './transact-delete' 6 | import { TransactPut } from './transact-put' 7 | import { TransactUpdate } from './transact-update' 8 | /** 9 | * @hidden 10 | */ 11 | export type TransactOperation = 12 | | TransactConditionCheck 13 | | TransactDelete 14 | | TransactPut 15 | | TransactUpdate 16 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-put.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateModel } from '../../../test/models' 2 | import { toDb } from '../../mapper/mapper' 3 | import { getTableName } from '../get-table-name.function' 4 | import { TransactPut } from './transact-put' 5 | 6 | describe('TransactPut', () => { 7 | let op: TransactPut 8 | let item: UpdateModel 9 | beforeEach(() => { 10 | const now = new Date() 11 | item = { 12 | id: 'myId', 13 | creationDate: now, 14 | lastUpdated: now, 15 | name: 'Foo Bar', 16 | active: true, 17 | counter: 10, 18 | addresses: [], 19 | numberValues: [42], 20 | info: { details: 'Foo Bar', createdAt: now }, 21 | informations: [{ details: 'My Details', createdAt: now }], 22 | topics: new Set(['Table-Tennis']), 23 | } 24 | op = new TransactPut(UpdateModel, item) 25 | }) 26 | 27 | it('correct transactItem', () => { 28 | expect(op.transactItem).toEqual({ 29 | Put: { 30 | TableName: getTableName(UpdateModel), 31 | Item: toDb(item, UpdateModel), 32 | }, 33 | }) 34 | }) 35 | 36 | it('ifNotExists should do nothing when predicate is falsy', () => { 37 | op.ifNotExists(false) 38 | expect(op.params).toEqual({ 39 | TableName: getTableName(UpdateModel), 40 | Item: toDb(item, UpdateModel), 41 | }) 42 | }) 43 | 44 | it('ifNotExists should add param when predicate is truthy (default)', () => { 45 | op.ifNotExists() 46 | expect(op.params).toEqual({ 47 | TableName: getTableName(UpdateModel), 48 | Item: toDb(item, UpdateModel), 49 | ConditionExpression: 'attribute_not_exists (#id)', 50 | ExpressionAttributeNames: { '#id': 'id' }, 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-put.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { toDb } from '../../mapper/mapper' 6 | import { ModelConstructor } from '../../model/model-constructor' 7 | import { createIfNotExistsCondition } from '../expression/create-if-not-exists-condition.function' 8 | import { TransactBaseOperation } from './transact-base-operation' 9 | 10 | /** 11 | * TransactOperation class for transactional put items. 12 | */ 13 | export class TransactPut extends TransactBaseOperation> { 14 | constructor(modelClazz: ModelConstructor, item: T) { 15 | super(modelClazz) 16 | this.params.Item = toDb(item, this.modelClazz) 17 | } 18 | 19 | /** 20 | * Adds a condition expression to the request, which makes sure the item will only be saved if the id does not exist 21 | */ 22 | ifNotExists(predicate: boolean = true): this { 23 | if (predicate) { 24 | this.onlyIf(...createIfNotExistsCondition(this.metadata)) 25 | } 26 | return this 27 | } 28 | 29 | get transactItem() { 30 | return { 31 | Put: { ...this.params }, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-update.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateModel } from '../../../test/models' 2 | import { Metadata } from '../../decorator/metadata/metadata' 3 | import { metadataForModel } from '../../decorator/metadata/metadata-for-model.function' 4 | import { createKeyAttributes } from '../../mapper/mapper' 5 | import { update2 } from '../expression/logical-operator/update.function' 6 | import { getTableName } from '../get-table-name.function' 7 | import { TransactUpdate } from './transact-update' 8 | 9 | describe('TransactUpdate', () => { 10 | let op: TransactUpdate 11 | let metadata: Metadata 12 | let now: Date 13 | beforeEach(() => { 14 | op = new TransactUpdate(UpdateModel, 'myId') 15 | now = new Date() 16 | metadata = metadataForModel(UpdateModel) 17 | }) 18 | 19 | afterEach(() => { 20 | expect(op.transactItem).toEqual({ 21 | Update: { 22 | TableName: getTableName(UpdateModel), 23 | Key: createKeyAttributes(metadata, 'myId'), 24 | 25 | UpdateExpression: 'SET #lastUpdated = if_not_exists(#lastUpdated, :lastUpdated)', 26 | ConditionExpression: '#name = :name', 27 | 28 | ExpressionAttributeNames: { 29 | '#lastUpdated': 'lastUpdated', 30 | '#name': 'name', 31 | }, 32 | ExpressionAttributeValues: { 33 | ':lastUpdated': { S: now.toISOString() }, 34 | ':name': { S: 'Foo Bar' }, 35 | }, 36 | }, 37 | }) 38 | }) 39 | 40 | it('correct transactItem [operations]', () => { 41 | op.operations(update2(UpdateModel, 'lastUpdated').set(now, true)).onlyIfAttribute('name').eq('Foo Bar') 42 | }) 43 | 44 | it('correct transactItem [updateAttribute]', () => { 45 | op.updateAttribute('lastUpdated').set(now, true).onlyIfAttribute('name').eq('Foo Bar') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/dynamo/transactwrite/transact-update.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module multi-model-requests/transact-write 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { createKeyAttributes } from '../../mapper/mapper' 6 | import { ModelConstructor } from '../../model/model-constructor' 7 | import { prepareAndAddUpdateExpressions } from '../expression/prepare-and-add-update-expressions.function' 8 | import { addUpdate } from '../expression/request-expression-builder' 9 | import { RequestUpdateFunction } from '../expression/type/update-expression-definition-chain' 10 | import { UpdateExpressionDefinitionFunction } from '../expression/type/update-expression-definition-function' 11 | import { TransactBaseOperation } from './transact-base-operation' 12 | 13 | /** 14 | * TransactOperation class for transactional update items. 15 | */ 16 | export class TransactUpdate extends TransactBaseOperation> { 17 | constructor(modelClazz: ModelConstructor, partitionKey: any, sortKey?: any) { 18 | super(modelClazz) 19 | this.params.Key = createKeyAttributes(this.metadata, partitionKey, sortKey) 20 | } 21 | 22 | /** 23 | * create and add a single update operation 24 | * @example updtTrans.updateAttribute('path.to.attr').set('newVal') 25 | */ 26 | updateAttribute(attributePath: K): RequestUpdateFunction { 27 | return addUpdate(attributePath, this, this.metadata) 28 | } 29 | 30 | /** 31 | * add multiple update ops 32 | * @example updtTrans.operations(update('path.to.attr).set('newVal'), ... ) 33 | */ 34 | operations(...updateDefFns: UpdateExpressionDefinitionFunction[]): this { 35 | prepareAndAddUpdateExpressions(this.metadata, this.params, updateDefFns) 36 | return this 37 | } 38 | 39 | get transactItem() { 40 | return { 41 | Update: { ...this.params }, 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/helper/curry.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { curry } from './curry.function' 2 | 3 | function a(x: number, y: string, z: boolean | null) { 4 | return [x, y, z] 5 | } 6 | 7 | describe('curry', () => { 8 | it('should work (w/o arity)', () => { 9 | expect(curry(a)(2)('ok')(true)).toEqual([2, 'ok', true]) 10 | expect(curry(a)(4, 'NOK')(false)).toEqual([4, 'NOK', false]) 11 | expect(curry(a)(6, 'FOO', null)).toEqual([6, 'FOO', null]) 12 | }) 13 | 14 | it('should work (w/ arity)', () => { 15 | expect(typeof curry(a, 4)(6, 'FOO', null)).toEqual('function') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/helper/extract-list-type.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | /** 5 | * extracts the type of an Array or Set. if neither array nor set, never is returned 6 | * 7 | * ExtractListType => string 8 | * ExtractListType> => string 9 | * @hidden 10 | */ 11 | export type ExtractListType = T extends Array ? A : T extends Set ? B : never 12 | -------------------------------------------------------------------------------- /src/helper/fetch-all.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | import * as DynamoDB from 'aws-sdk/clients/dynamodb' 5 | import { QueryRequest } from '../dynamo/request/query/query.request' 6 | import { ReadManyRequest } from '../dynamo/request/read-many.request' 7 | import { ScanRequest } from '../dynamo/request/scan/scan.request' 8 | 9 | /** 10 | * When we cant load all the items of a table with one request, we will fetch as long as there is more data 11 | * available. This can be used with scan and query requests. 12 | */ 13 | 14 | export function fetchAll(request: ScanRequest | QueryRequest, startKey?: DynamoDB.Key): Promise { 15 | request.limit(ReadManyRequest.INFINITE_LIMIT) 16 | if (startKey) { 17 | request.exclusiveStartKey(startKey) 18 | } 19 | return request.execFullResponse().then((response) => { 20 | if (response.LastEvaluatedKey) { 21 | return fetchAll(request, response.LastEvaluatedKey).then((innerResponse) => [...response.Items, ...innerResponse]) 22 | } else { 23 | return response.Items 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/helper/get-tag.function.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from './tag.enum' 2 | 3 | /** 4 | * @return Returns the value (we call it tag) returned by function call `value.toString`, 5 | */ 6 | export function getTag(value: any): Tag | string { 7 | return Object.prototype.toString.call(value) 8 | } 9 | -------------------------------------------------------------------------------- /src/helper/is-boolean.function.ts: -------------------------------------------------------------------------------- 1 | import { getTag } from './get-tag.function' 2 | import { Tag } from './tag.enum' 3 | 4 | /** 5 | * @return Returns true for any value where typeof equals 'string' or an object created with String constructor 6 | */ 7 | export function isBoolean(value: any): boolean { 8 | return typeof value === 'boolean' || getTag(value) === Tag.BOOLEAN 9 | } 10 | -------------------------------------------------------------------------------- /src/helper/is-boolean.spec.ts: -------------------------------------------------------------------------------- 1 | import { isBoolean } from './is-boolean.function' 2 | 3 | describe('is boolean', () => { 4 | it('should be a boolean', () => { 5 | expect(isBoolean(true)).toBeTruthy() 6 | expect(isBoolean(false)).toBeTruthy() 7 | // tslint:disable:no-construct 8 | expect(isBoolean(new Boolean(1))).toBeTruthy() 9 | expect(isBoolean(new Boolean(0))).toBeTruthy() 10 | }) 11 | 12 | it('should not be a boolean', () => { 13 | expect(isBoolean(0)).toBeFalsy() 14 | expect(isBoolean(1)).toBeFalsy() 15 | expect(isBoolean('a')).toBeFalsy() 16 | expect(isBoolean({})).toBeFalsy() 17 | expect(isBoolean([])).toBeFalsy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/helper/is-empty.function.ts: -------------------------------------------------------------------------------- 1 | export function isEmpty(val?: object | string): boolean { 2 | return Object.keys(val || {}).length === 0 3 | } 4 | -------------------------------------------------------------------------------- /src/helper/is-empty.spec.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from './is-empty.function' 2 | 3 | describe('isEmpty', () => { 4 | it('should work', () => { 5 | expect(isEmpty({})).toBeTruthy() 6 | expect(isEmpty({ ok: true })).toBeFalsy() 7 | expect(isEmpty('')).toBeTruthy() 8 | expect(isEmpty('ok')).toBeFalsy() 9 | expect(isEmpty()).toBeTruthy() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/helper/is-number.function.ts: -------------------------------------------------------------------------------- 1 | import { getTag } from './get-tag.function' 2 | import { Tag } from './tag.enum' 3 | 4 | /** 5 | * @return Returns true for any value where typeof equals 'number' or an object created with Number constructor 6 | */ 7 | export function isNumber(value: any): boolean { 8 | return typeof value === 'number' || getTag(value) === Tag.NUMBER 9 | } 10 | -------------------------------------------------------------------------------- /src/helper/is-number.spec.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from './is-number.function' 2 | 3 | describe('is number', () => { 4 | it('should be a number', () => { 5 | expect(isNumber(3)).toBeTruthy() 6 | expect(isNumber(NaN)).toBeTruthy() 7 | expect(isNumber(Infinity)).toBeTruthy() 8 | // tslint:disable:no-construct 9 | expect(isNumber(new Number('2'))).toBeTruthy() 10 | expect(isNumber(new Number('myNumber'))).toBeTruthy() 11 | }) 12 | 13 | it('should not be a number', () => { 14 | expect(isNumber('a')).toBeFalsy() 15 | expect(isNumber({})).toBeFalsy() 16 | expect(isNumber([])).toBeFalsy() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/helper/is-plain-object.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from './is-plain-object.function' 2 | 3 | class Foo { 4 | a = 1 5 | } 6 | 7 | describe('isPlainObject', () => { 8 | it('should work', () => { 9 | expect(isPlainObject({})).toBeTruthy() 10 | expect(isPlainObject(Object.create({}))).toBeTruthy() 11 | expect(isPlainObject(Object.create(Object.prototype))).toBeTruthy() 12 | expect(isPlainObject({ x: 0, y: 0 })).toBeTruthy() 13 | 14 | expect(isPlainObject([])).toBeFalsy() 15 | expect(isPlainObject([1, 2, 3])).toBeFalsy() 16 | expect(isPlainObject(1)).toBeFalsy() 17 | expect(isPlainObject(null)).toBeFalsy() 18 | expect(isPlainObject(Object.create(null))).toBeFalsy() 19 | expect(isPlainObject(new Foo())).toBeFalsy() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/helper/is-plain-object.function.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/jonschlinkert/is-plain-object 2 | import { getTag } from './get-tag.function' 3 | import { Tag } from './tag.enum' 4 | 5 | function isObject(val: any) { 6 | return val != null && typeof val === 'object' && Array.isArray(val) === false 7 | } 8 | 9 | function isObjectObject(o: any): boolean { 10 | return isObject(o) === true && getTag(o) === Tag.OBJECT 11 | } 12 | 13 | export function isPlainObject(o: any): boolean { 14 | return !( 15 | !isObjectObject(o) || 16 | typeof o.constructor !== 'function' || 17 | !isObjectObject(o.constructor.prototype) || 18 | !o.constructor.prototype.hasOwnProperty('isPrototypeOf') 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/helper/is-string.function.ts: -------------------------------------------------------------------------------- 1 | import { getTag } from './get-tag.function' 2 | import { Tag } from './tag.enum' 3 | 4 | /** 5 | * @return Returns true for any value where typeof equals 'string' or an object created with String constructor 6 | */ 7 | export function isString(value: any): boolean { 8 | return typeof value === 'string' || getTag(value) === Tag.STRING 9 | } 10 | -------------------------------------------------------------------------------- /src/helper/is-string.spec.ts: -------------------------------------------------------------------------------- 1 | import { isString } from './is-string.function' 2 | 3 | describe('is string', () => { 4 | it('should be a string', () => { 5 | expect(isString('myValue')).toBeTruthy() 6 | // tslint:disable:no-construct 7 | expect(isString(new String('2'))).toBeTruthy() 8 | expect(isString(new String('someValue'))).toBeTruthy() 9 | }) 10 | 11 | it('should not be a string', () => { 12 | expect(isString(3)).toBeFalsy() 13 | expect(isString(true)).toBeFalsy() 14 | expect(isString({})).toBeFalsy() 15 | expect(isString([])).toBeFalsy() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/helper/kebab-case.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { kebabCase } from './kebab-case.function' 2 | 3 | describe('kebabCase', () => { 4 | it('should work', () => { 5 | expect(kebabCase('the quick brown fox')).toBe('the-quick-brown-fox') 6 | expect(kebabCase('the-quick-brown-fox')).toBe('the-quick-brown-fox') 7 | expect(kebabCase('the_quick_brown_fox')).toBe('the-quick-brown-fox') 8 | expect(kebabCase('theQuickBrownFox')).toBe('the-quick-brown-fox') 9 | expect(kebabCase('theQuickBrown Fox')).toBe('the-quick-brown-fox') 10 | expect(kebabCase('thequickbrownfox')).toBe('thequickbrownfox') 11 | expect(kebabCase('the - quick * brown# fox')).toBe('the-quick-brown-fox') 12 | expect(kebabCase('theQUICKBrownFox')).toBe('the-q-u-i-c-k-brown-fox') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/helper/kebab-case.function.ts: -------------------------------------------------------------------------------- 1 | // copied from just-kebab-case 2 | 3 | // any combination of spaces and punctuation characters 4 | // thanks to http://stackoverflow.com/a/25575009 5 | const wordSeparators = /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/ 6 | const capitals = /[A-Z\u00C0-\u00D6\u00D9-\u00DD]/g 7 | 8 | /** 9 | * replace capitals with space + lower case equivalent for later parsing 10 | */ 11 | export function kebabCase(str: string): string { 12 | return str 13 | .replace(capitals, (match) => ' ' + (match.toLowerCase() || match)) 14 | .trim() 15 | .split(wordSeparators) 16 | .join('-') 17 | } 18 | -------------------------------------------------------------------------------- /src/helper/not-null.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | export function notNull(value: TValue | null): value is TValue { 8 | return value !== null 9 | } 10 | -------------------------------------------------------------------------------- /src/helper/promise-delay.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseDelay } from './promise-delay.function' 2 | 3 | describe('PromiseDelay', () => { 4 | const myVal = { myVal: true } 5 | const delay = 300 6 | 7 | it('should delay a promise value', async () => { 8 | const startTime = Date.now() 9 | const result = await Promise.resolve(myVal).then(promiseDelay(delay)) 10 | const endTime = Date.now() 11 | 12 | expect(result).toEqual(myVal) 13 | expect(endTime - startTime >= delay).toBeTruthy() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/helper/promise-delay.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | /** 5 | * Will resolve after given duration 6 | * @hidden 7 | */ 8 | export function promiseDelay(duration: number): (arg: T) => Promise { 9 | return (arg: T) => { 10 | return new Promise((resolve, reject) => { 11 | setTimeout(() => resolve(arg), duration) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/helper/promise-tap.function.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseTap } from './promise-tap.function' 2 | 3 | describe('PromiseTap', () => { 4 | const myVal = { myVal: true } 5 | 6 | it('should exec the given function but return the initial value', async () => { 7 | const spyFn = jasmine.createSpy().and.returnValue(null) 8 | 9 | const result = await Promise.resolve(myVal).then(promiseTap(spyFn)) 10 | 11 | expect(spyFn).toHaveBeenCalled() 12 | expect(result).toEqual(myVal) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/helper/promise-tap.function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | /** 5 | * mimics the tap operator from rxjs, will execute some side effect and return the input value 6 | * @hidden 7 | */ 8 | export function promiseTap(tapFunction: (arg: T) => void): (arg: T) => Promise { 9 | return (arg: T) => { 10 | tapFunction(arg) 11 | return Promise.resolve(arg) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/helper/random-exponential-backoff-timer.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomExponentialBackoffTimer } from './random-exponential-backoff-timer.generator' 2 | 3 | describe('random exponential backoff timer', () => { 4 | let g: IterableIterator 5 | 6 | beforeEach(() => (g = randomExponentialBackoffTimer())) 7 | 8 | it('should generate randomly values that are getting larger', () => { 9 | expect(g.next().value).toBeLessThanOrEqual(0.5) 10 | expect(g.next().value).toBeLessThanOrEqual(1.5) 11 | expect(g.next().value).toBeLessThanOrEqual(3.5) 12 | expect(g.next().value).toBeLessThanOrEqual(7.5) 13 | expect(g.next().done).toBe(false) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/helper/random-exponential-backoff-timer.generator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module helper 3 | */ 4 | /** 5 | * returns a random value from an increasing range by each iteration. 6 | */ 7 | export function* randomExponentialBackoffTimer() { 8 | let i = 0 9 | while (true) { 10 | yield (Math.pow(2, Math.round(Math.random() * ++i)) - 1) / 2 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helper/tag.enum.ts: -------------------------------------------------------------------------------- 1 | /** used to compare with value returned by @link getTag(value), this is not a complete list only the one values used are defined */ 2 | export enum Tag { 3 | OBJECT = '[object Object]', 4 | STRING = '[object String]', 5 | NUMBER = '[object Number]', 6 | BOOLEAN = '[object Boolean]', 7 | } 8 | -------------------------------------------------------------------------------- /src/logger/default-log-receiver.const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module logger 3 | */ 4 | /** 5 | * @hidden 6 | */ 7 | // tslint:disable-next-line:no-empty 8 | export const DEFAULT_LOG_RECEIVER = () => {} 9 | -------------------------------------------------------------------------------- /src/logger/log-info.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module logger 3 | */ 4 | import { LogLevel } from './log-level.type' 5 | 6 | /** 7 | * type for log statements 8 | */ 9 | export interface LogInfo { 10 | className: string 11 | modelConstructor: string 12 | level: LogLevel 13 | message: string 14 | timestamp: number 15 | data?: any 16 | } 17 | -------------------------------------------------------------------------------- /src/logger/log-level.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module logger 3 | */ 4 | /** 5 | * LogLevel 6 | */ 7 | export enum LogLevel { 8 | // ERROR = 1, // currently not used, since errors are thrown 9 | WARNING = 2, 10 | INFO = 3, 11 | DEBUG = 4, 12 | VERBOSE = 5, 13 | } 14 | -------------------------------------------------------------------------------- /src/logger/log-receiver.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module logger 3 | */ 4 | import { LogInfo } from './log-info.type' 5 | 6 | export type LogReceiver = (logInfo: LogInfo) => any | void 7 | -------------------------------------------------------------------------------- /src/logger/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module logger 3 | */ 4 | export * from './log-info.type' 5 | export * from './log-level.type' 6 | export * from './log-receiver.type' 7 | -------------------------------------------------------------------------------- /src/mapper/custom/date-to-number.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { MapperForType } from '../for-type/base.mapper' 2 | import { NumberAttribute } from '../type/attribute.type' 3 | import { dateToNumberMapper } from './date-to-number.mapper' 4 | 5 | describe('date mapper', () => { 6 | let dateMapper: MapperForType 7 | 8 | beforeEach(() => { 9 | dateMapper = dateToNumberMapper 10 | }) 11 | 12 | describe('to db', () => { 13 | it('simple', () => { 14 | const now = new Date() 15 | const toDb = dateMapper.toDb(now) 16 | 17 | expect(toDb).toBeDefined() 18 | expect(toDb).toEqual({ N: `${now.getTime()}` }) 19 | }) 20 | 21 | it('throws', () => { 22 | expect(() => dateMapper.toDb('noDate')).toThrowError() 23 | }) 24 | }) 25 | 26 | describe('from db', () => { 27 | it('simple', () => { 28 | const now = new Date() 29 | const fromDb = dateMapper.fromDb({ N: `${now.getTime()}` }) 30 | expect(fromDb).toBeDefined() 31 | expect(fromDb).toEqual(now) 32 | }) 33 | 34 | it('throws', () => { 35 | expect(() => dateMapper.fromDb({ S: 'noDate' })).toThrowError() 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/mapper/custom/date-to-number.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { MapperForType } from '../for-type/base.mapper' 5 | import { NumberAttribute } from '../type/attribute.type' 6 | 7 | function dateFromDb(attributeValue: NumberAttribute): Date { 8 | if (attributeValue.N) { 9 | return new Date(parseInt(attributeValue.N, 10)) 10 | } else { 11 | throw new Error('there is no N(umber) value defined on given attribute value') 12 | } 13 | } 14 | 15 | function dateToDb(modelValue: Date): NumberAttribute { 16 | // noinspection SuspiciousInstanceOfGuard 17 | if (modelValue && modelValue instanceof Date) { 18 | return { N: `${modelValue.getTime()}` } 19 | } else { 20 | throw new Error('the given model value must be an instance of Date') 21 | } 22 | } 23 | 24 | export const dateToNumberMapper: MapperForType = { 25 | fromDb: dateFromDb, 26 | toDb: dateToDb, 27 | } 28 | -------------------------------------------------------------------------------- /src/mapper/custom/date-to-string.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-non-null-assertion 2 | import { dateToStringMapper } from './date-to-string.mapper' 3 | 4 | describe('dateToStringMapper', () => { 5 | describe('to db', () => { 6 | it('simple', () => { 7 | const now = new Date() 8 | const toDb = dateToStringMapper.toDb(now) 9 | expect(toDb).toBeDefined() 10 | expect(toDb!.S).toBeDefined() 11 | expect(toDb!.S).toEqual(`${now.toISOString()}`) 12 | }) 13 | 14 | it('throws', () => { 15 | expect(() => dateToStringMapper.toDb('noDate')).toThrowError() 16 | }) 17 | }) 18 | 19 | describe('from db', () => { 20 | it('simple', () => { 21 | const now = new Date() 22 | const fromDb = dateToStringMapper.fromDb({ S: `${now.toISOString()}` }) 23 | 24 | expect(fromDb).toEqual(now) 25 | }) 26 | 27 | it('throws', () => { 28 | expect(() => dateToStringMapper.fromDb({ N: '4545' })).toThrowError() 29 | expect(() => dateToStringMapper.fromDb({ S: 'noDate' })).toThrowError() 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/mapper/custom/date-to-string.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { MapperForType } from '../for-type/base.mapper' 5 | import { StringAttribute } from '../type/attribute.type' 6 | 7 | function dateFromDb(attributeValue: StringAttribute): Date { 8 | if (attributeValue.S) { 9 | const date = new Date(attributeValue.S) 10 | if (isNaN(date)) { 11 | throw new Error('given string is not a valid date string') 12 | } 13 | return date 14 | } else { 15 | throw new Error('there is no S(tring) value defined on given attribute value') 16 | } 17 | } 18 | 19 | function dateToDb(modelValue: Date): StringAttribute { 20 | // noinspection SuspiciousInstanceOfGuard 21 | if (modelValue && modelValue instanceof Date) { 22 | return { S: `${modelValue.toISOString()}` } 23 | } else { 24 | throw new Error('the given model value must be an instance of Date') 25 | } 26 | } 27 | 28 | export const dateToStringMapper: MapperForType = { 29 | fromDb: dateFromDb, 30 | toDb: dateToDb, 31 | } 32 | -------------------------------------------------------------------------------- /src/mapper/for-type/base.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' 5 | import { 6 | BinaryAttribute, 7 | BinarySetAttribute, 8 | BooleanAttribute, 9 | ListAttribute, 10 | MapAttribute, 11 | NullAttribute, 12 | NumberAttribute, 13 | NumberSetAttribute, 14 | StringAttribute, 15 | StringSetAttribute, 16 | } from '../type/attribute.type' 17 | 18 | /** 19 | * Maps a js value to an attribute value so it can be stored in dynamoDB, supported types are 20 | * 21 | * S(tring) 22 | * N(umber) 23 | * B(inary) 24 | * BOOL 25 | * NULL 26 | * S(tring)S(et) 27 | * N(umber)S(et) 28 | * B(inary)S(et) 29 | * L(ist) 30 | */ 31 | export type ToDbFn< 32 | T, 33 | R extends 34 | | StringAttribute 35 | | NumberAttribute 36 | | BinaryAttribute 37 | | StringSetAttribute 38 | | NumberSetAttribute 39 | | BinarySetAttribute 40 | | MapAttribute 41 | | ListAttribute 42 | | NullAttribute 43 | | BooleanAttribute 44 | > = (propertyValue: T, propertyMetadata?: PropertyMetadata) => R | null 45 | 46 | /** 47 | * Maps an attribute value coming from dynamoDB to an javascript type 48 | */ 49 | export type FromDbFn< 50 | T, 51 | R extends 52 | | StringAttribute 53 | | NumberAttribute 54 | | BinaryAttribute 55 | | StringSetAttribute 56 | | NumberSetAttribute 57 | | BinarySetAttribute 58 | | MapAttribute 59 | | ListAttribute 60 | | NullAttribute 61 | | BooleanAttribute 62 | > = (attributeValue: R, propertyMetadata?: PropertyMetadata) => T 63 | 64 | /** 65 | * A Mapper is responsible to define how a specific type is mapped to an attribute value which can be stored in dynamoDB and how to parse the value from 66 | * dynamoDB back into the specific type 67 | */ 68 | export interface MapperForType< 69 | T, 70 | R extends 71 | | StringAttribute 72 | | NumberAttribute 73 | | BinaryAttribute 74 | | StringSetAttribute 75 | | NumberSetAttribute 76 | | BinarySetAttribute 77 | | MapAttribute 78 | | ListAttribute 79 | | NullAttribute 80 | | BooleanAttribute 81 | > { 82 | fromDb: FromDbFn 83 | toDb: ToDbFn 84 | } 85 | -------------------------------------------------------------------------------- /src/mapper/for-type/boolean.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { BooleanMapper } from './boolean.mapper' 2 | 3 | describe('boolean mapper', () => { 4 | describe('to db', () => { 5 | it('should work (true)', () => { 6 | const attributeValue = BooleanMapper.toDb(true) 7 | expect(attributeValue).toEqual({ BOOL: true }) 8 | }) 9 | 10 | it('should work (false)', () => { 11 | const attributeValue = BooleanMapper.toDb(false) 12 | expect(attributeValue).toEqual({ BOOL: false }) 13 | }) 14 | 15 | it('should throw (string is not a valid boolean value)', () => { 16 | expect(() => { 17 | BooleanMapper.toDb('true') 18 | }).toThrowError() 19 | }) 20 | 21 | it('should throw (string is not a valid boolean value)', () => { 22 | expect(() => { 23 | BooleanMapper.toDb(1) 24 | }).toThrowError() 25 | }) 26 | }) 27 | 28 | describe('from db', () => { 29 | it('should work (true)', () => { 30 | const enumValue = BooleanMapper.fromDb({ BOOL: true }) 31 | expect(enumValue).toBe(true) 32 | }) 33 | 34 | it('should work (false)', () => { 35 | const enumValue = BooleanMapper.fromDb({ BOOL: false }) 36 | expect(enumValue).toBe(false) 37 | }) 38 | 39 | it('should throw (S cannot be mapped to boolean)', () => { 40 | expect(() => { 41 | BooleanMapper.fromDb({ S: 'true' }) 42 | }).toThrowError() 43 | }) 44 | 45 | it('should throw (N cannot be mapped to boolean)', () => { 46 | expect(() => { 47 | BooleanMapper.fromDb({ N: '1' }) 48 | }).toThrowError() 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/mapper/for-type/boolean.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { BooleanAttribute } from '../type/attribute.type' 5 | import { MapperForType } from './base.mapper' 6 | 7 | function booleanFromDb(attributeValue: BooleanAttribute): boolean { 8 | if (attributeValue.BOOL === undefined) { 9 | throw new Error(`there is no BOOL(ean) value defined on given attribute value: ${JSON.stringify(attributeValue)}`) 10 | } 11 | return attributeValue.BOOL === true 12 | } 13 | 14 | function booleanToDb(modelValue: boolean): BooleanAttribute { 15 | if (!(modelValue === true || modelValue === false)) { 16 | throw new Error(`only boolean values are mapped to a BOOl attribute, given: ${JSON.stringify(modelValue)}`) 17 | } 18 | return { BOOL: modelValue } 19 | } 20 | 21 | export const BooleanMapper: MapperForType = { 22 | fromDb: booleanFromDb, 23 | toDb: booleanToDb, 24 | } 25 | -------------------------------------------------------------------------------- /src/mapper/for-type/enum.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { hasGenericType, PropertyMetadata } from '../../decorator/metadata/property-metadata.model' 5 | import { NumberAttribute } from '../type/attribute.type' 6 | import { MapperForType } from './base.mapper' 7 | 8 | function enumToDb(value: string | number, propertyMetadata?: PropertyMetadata): NumberAttribute { 9 | if (Number.isInteger(value)) { 10 | if (hasGenericType(propertyMetadata) && (propertyMetadata.typeInfo.genericType)[value] === undefined) { 11 | throw new Error(`${JSON.stringify(value)} is not a valid value for enum ${propertyMetadata.typeInfo.genericType}`) 12 | } 13 | return { N: value.toString() } 14 | } else { 15 | throw new Error(`only integer is a supported value for an enum, given value: ${JSON.stringify(value)}`) 16 | } 17 | } 18 | 19 | function enumFromDb( 20 | attributeValue: NumberAttribute, 21 | propertyMetadata?: PropertyMetadata, 22 | ): string | number { 23 | if (!isNaN(parseInt(attributeValue.N, 10))) { 24 | const enumValue = parseInt(attributeValue.N, 10) 25 | if (propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.genericType) { 26 | if ((propertyMetadata.typeInfo.genericType)[enumValue] === undefined) { 27 | throw new Error( 28 | `${enumValue} is not a valid value for enum ${JSON.stringify(propertyMetadata.typeInfo.genericType)}`, 29 | ) 30 | } 31 | } 32 | 33 | return enumValue 34 | } else { 35 | throw new Error( 36 | `make sure the value is a N(umber), which is the only supported for EnumMapper right now, given attributeValue: ${JSON.stringify( 37 | attributeValue, 38 | )}`, 39 | ) 40 | } 41 | } 42 | 43 | /** 44 | * Enums are mapped to numbers by default. 45 | * ensures given value is from enum, if enum was specified as generic type 46 | */ 47 | export const EnumMapper: MapperForType = { 48 | fromDb: enumFromDb, 49 | toDb: enumToDb, 50 | } 51 | -------------------------------------------------------------------------------- /src/mapper/for-type/null.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { NullMapper } from './null.mapper' 2 | 3 | describe('null mapper', () => { 4 | describe('to db', () => { 5 | it('should work', () => { 6 | const attributeValue = NullMapper.toDb(null) 7 | expect(attributeValue).toEqual({ NULL: true }) 8 | }) 9 | 10 | it('should throw (invalid null value)', () => { 11 | expect(() => { 12 | NullMapper.toDb('stringValue') 13 | }).toThrowError() 14 | }) 15 | }) 16 | 17 | describe('from db', () => { 18 | it('should work', () => { 19 | const nullValue: null = NullMapper.fromDb({ NULL: true }) 20 | expect(nullValue).toBe(null) 21 | }) 22 | 23 | it('should throw (no null value)', () => { 24 | expect(() => { 25 | NullMapper.fromDb({ S: 'nullValue' }) 26 | }).toThrowError() 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/mapper/for-type/null.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { NullAttribute } from '../type/attribute.type' 5 | import { MapperForType } from './base.mapper' 6 | 7 | function nullFromDb(attributeValue: NullAttribute): null { 8 | if (attributeValue.NULL) { 9 | return null 10 | } else { 11 | throw new Error(`there is no NULL value defined on given attribute value: ${JSON.stringify(attributeValue)}`) 12 | } 13 | } 14 | 15 | function nullToDb(value: null): NullAttribute { 16 | if (value !== null) { 17 | throw new Error(`null mapper only supports null value, got ${JSON.stringify(value)}`) 18 | } 19 | 20 | return { NULL: true } 21 | } 22 | 23 | export const NullMapper: MapperForType = { 24 | fromDb: nullFromDb, 25 | toDb: nullToDb, 26 | } 27 | -------------------------------------------------------------------------------- /src/mapper/for-type/number.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { NumberMapper } from './number.mapper' 2 | 3 | describe('number mapper', () => { 4 | describe('to db', () => { 5 | it('should work', () => { 6 | const attributeValue = NumberMapper.toDb(25) 7 | expect(attributeValue).toEqual({ N: '25' }) 8 | }) 9 | 10 | it('should throw (invalid number value)', () => { 11 | expect(() => { 12 | NumberMapper.toDb('25') 13 | }).toThrowError() 14 | }) 15 | }) 16 | 17 | describe('from db', () => { 18 | it('should work', () => { 19 | const numberValue: number = NumberMapper.fromDb({ N: '56' }) 20 | expect(numberValue).toBe(56) 21 | }) 22 | 23 | it('should throw (no number value)', () => { 24 | expect(() => { 25 | NumberMapper.fromDb({ N: 'noNumber' }) 26 | }).toThrowError() 27 | }) 28 | 29 | it('should throw (no number value)', () => { 30 | expect(() => { 31 | NumberMapper.fromDb({ S: '56' }) 32 | }).toThrowError() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/mapper/for-type/number.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { isNumber } from '../../helper/is-number.function' 5 | import { NumberAttribute } from '../type/attribute.type' 6 | import { MapperForType } from './base.mapper' 7 | 8 | function numberFromDb(attributeValue: NumberAttribute): number { 9 | if (attributeValue.N) { 10 | const numberValue = Number.parseFloat(attributeValue.N) 11 | if (isNaN(numberValue)) { 12 | throw new Error(`value ${attributeValue.N} resolves to NaN when parsing using Number.parseFloat`) 13 | } 14 | return numberValue 15 | } else { 16 | throw new Error(`there is no N(umber) value defined on given attribute value: ${JSON.stringify(attributeValue)}`) 17 | } 18 | } 19 | 20 | function numberToDb(modelValue: number): NumberAttribute | null { 21 | if (!isNumber(modelValue)) { 22 | throw new Error(`this mapper only support values of type number, value given: ${JSON.stringify(modelValue)}`) 23 | } 24 | 25 | if (isNaN(modelValue)) { 26 | return null 27 | } 28 | 29 | return { N: modelValue.toString() } 30 | } 31 | 32 | export const NumberMapper: MapperForType = { 33 | fromDb: numberFromDb, 34 | toDb: numberToDb, 35 | } 36 | -------------------------------------------------------------------------------- /src/mapper/for-type/object.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectMapper } from './object.mapper' 2 | 3 | describe('object mapper', () => { 4 | describe('to db', () => { 5 | it('should work', () => { 6 | const attributeValue = ObjectMapper.toDb({ name: 'name', age: 45, active: true }) 7 | expect(attributeValue).toEqual({ M: { name: { S: 'name' }, age: { N: '45' }, active: { BOOL: true } } }) 8 | }) 9 | }) 10 | 11 | describe('from db', () => { 12 | it('should work', () => { 13 | const objectValue: any = ObjectMapper.fromDb({ 14 | M: { name: { S: 'name' }, age: { N: '45' }, active: { BOOL: true } }, 15 | }) 16 | expect(objectValue).toEqual({ name: 'name', age: 45, active: true }) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/mapper/for-type/object.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { hasType, PropertyMetadata } from '../../decorator/metadata/property-metadata.model' 5 | import { fromDb, toDb } from '../mapper' 6 | import { Attributes, MapAttribute } from '../type/attribute.type' 7 | import { MapperForType } from './base.mapper' 8 | 9 | function objectFromDb(val: MapAttribute, propertyMetadata?: PropertyMetadata): any { 10 | // todo: shouldn't we check for existence off 'M' here? (and throw if undefined) 11 | if (hasType(propertyMetadata)) { 12 | return fromDb(val.M, propertyMetadata.typeInfo.type) 13 | } else { 14 | return fromDb(val.M) 15 | } 16 | } 17 | 18 | function objectToDb(modelValue: any, propertyMetadata?: PropertyMetadata): MapAttribute { 19 | let value: Attributes 20 | if (hasType(propertyMetadata)) { 21 | value = toDb(modelValue, propertyMetadata.typeInfo.type) 22 | } else { 23 | value = toDb(modelValue) 24 | } 25 | 26 | return { M: value } 27 | } 28 | 29 | export const ObjectMapper: MapperForType = { 30 | fromDb: objectFromDb, 31 | toDb: objectToDb, 32 | } 33 | -------------------------------------------------------------------------------- /src/mapper/for-type/string.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { StringMapper } from './string.mapper' 2 | 3 | describe('string mapper', () => { 4 | describe('to db', () => { 5 | it('should work', () => { 6 | const attributeValue = StringMapper.toDb('myStringValue') 7 | expect(attributeValue).toEqual({ S: 'myStringValue' }) 8 | }) 9 | 10 | it('should work (empty string)', () => { 11 | const attributeValue = StringMapper.toDb('') 12 | expect(attributeValue).toStrictEqual({ S: '' }) 13 | }) 14 | 15 | it('should work (null)', () => { 16 | const attributeValue = StringMapper.toDb(null) 17 | expect(attributeValue).toBe(null) 18 | }) 19 | 20 | it('should work (undefined)', () => { 21 | const attributeValue = StringMapper.toDb(undefined) 22 | expect(attributeValue).toBe(null) 23 | }) 24 | }) 25 | 26 | describe('from db', () => { 27 | it('should work', () => { 28 | const stringValue = StringMapper.fromDb({ S: 'myStringValue' }) 29 | expect(stringValue).toBe('myStringValue') 30 | }) 31 | it('should allow empty string values', () => { 32 | const stringValue = StringMapper.fromDb({ S: '' }) 33 | expect(stringValue).toBe('') 34 | }) 35 | it('should throw if not a string attribute', () => { 36 | expect(() => StringMapper.fromDb({ N: '8' })).toThrow() 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/mapper/for-type/string.mapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { StringAttribute } from '../type/attribute.type' 5 | import { MapperForType } from './base.mapper' 6 | 7 | function stringFromDb(attributeValue: StringAttribute): string { 8 | if (attributeValue.S || attributeValue.S === '') { 9 | return attributeValue.S 10 | } else { 11 | throw new Error(`there is no S(tring) value defined on given attribute value: ${JSON.stringify(attributeValue)}`) 12 | } 13 | } 14 | 15 | function stringToDb(modelValue: string): StringAttribute | null { 16 | // an empty string is valid for a string attribute 17 | if (modelValue === null || modelValue === undefined) { 18 | return null 19 | } else { 20 | return { S: modelValue } 21 | } 22 | } 23 | 24 | export const StringMapper: MapperForType = { 25 | fromDb: stringFromDb, 26 | toDb: stringToDb, 27 | } 28 | -------------------------------------------------------------------------------- /src/mapper/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | export * from './type/attribute.type' 5 | export * from './type/attribute-value-type.type' 6 | export * from './type/attribute-type.type' 7 | export * from './mapper' 8 | export * from './custom/date-to-string.mapper' 9 | export * from './custom/date-to-number.mapper' 10 | export * from './type/null.type' 11 | export * from './type/undefined.type' 12 | export * from './type/binary.type' 13 | export * from './util' 14 | export * from './for-type/base.mapper' 15 | export * from './for-type/string.mapper' 16 | export * from './for-type/number.mapper' 17 | export * from './for-type/boolean.mapper' 18 | export * from './for-type/collection.mapper' 19 | export * from './for-type/object.mapper' 20 | export * from './for-type/enum.mapper' 21 | export * from './for-type/null.mapper' 22 | -------------------------------------------------------------------------------- /src/mapper/type/attribute-type.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | export type AttributeCollectionType = 'SS' | 'NS' | 'BS' | 'L' 5 | 6 | export type AttributeType = 'S' | 'N' | 'B' | 'M' | 'NULL' | 'BOOL' | AttributeCollectionType 7 | -------------------------------------------------------------------------------- /src/mapper/type/attribute-value-type.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | import { Binary } from './binary.type' 5 | import { NullType } from './null.type' 6 | import { UndefinedType } from './undefined.type' 7 | 8 | export type AttributeValueType = 9 | | string 10 | | number 11 | | boolean 12 | | Binary 13 | | Set 14 | | Map 15 | | any[] 16 | | NullType 17 | | UndefinedType 18 | | object 19 | -------------------------------------------------------------------------------- /src/mapper/type/binary.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | // tslint:disable:no-unnecessary-class 5 | export class Binary {} 6 | -------------------------------------------------------------------------------- /src/mapper/type/null.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | // tslint:disable:no-unnecessary-class 5 | export class NullType {} 6 | -------------------------------------------------------------------------------- /src/mapper/type/undefined.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mapper 3 | */ 4 | // tslint:disable:no-unnecessary-class 5 | export class UndefinedType {} 6 | -------------------------------------------------------------------------------- /src/model/model-constructor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | /** 5 | * Type for model class 6 | */ 7 | export type ModelConstructor = new (...args: any[]) => T 8 | -------------------------------------------------------------------------------- /src/model/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module dynamo-easy 3 | */ 4 | export * from './model-constructor' 5 | -------------------------------------------------------------------------------- /test/data/organization-dynamodb.data.ts: -------------------------------------------------------------------------------- 1 | import { Attributes } from '../../src/dynamo-easy' 2 | import { Organization } from '../models' 3 | 4 | export const organization1CreatedAt = new Date('2017-05-15') 5 | export const organization1LastUpdated = new Date('2017-07-25') 6 | export const organization1Employee1CreatedAt = new Date('2015-02-15') 7 | export const organization1Employee2CreatedAt = new Date('2015-07-03') 8 | 9 | export const organizationFromDb: Attributes = { 10 | name: { S: 'myOrganization' }, 11 | id: { S: 'myId' }, 12 | transient: { NULL: true }, 13 | createdAtDate: { 14 | S: organization1CreatedAt.toISOString(), 15 | }, 16 | lastUpdated: { 17 | S: organization1LastUpdated.toISOString(), 18 | }, 19 | active: { BOOL: true }, 20 | count: { N: '52' }, 21 | employees: { 22 | L: [ 23 | { 24 | M: { 25 | name: { S: 'max' }, 26 | age: { N: '50' }, 27 | createdAt: { 28 | S: organization1Employee1CreatedAt.toISOString(), 29 | }, 30 | sortedSet: { L: [{ S: 'first' }, { S: 'third' }, { S: 'second' }] }, 31 | }, 32 | }, 33 | { 34 | M: { 35 | name: { S: 'anna' }, 36 | age: { N: '27' }, 37 | createdAt: { 38 | S: organization1Employee2CreatedAt.toISOString(), 39 | }, 40 | sortedSet: { L: [{ S: 'first' }, { S: 'third' }, { S: 'second' }] }, 41 | }, 42 | }, 43 | ], 44 | }, 45 | cities: { SS: ['zürich', 'bern'] }, 46 | domains: { SS: ['myOrg.ch', 'myOrg.com'] }, 47 | randomDetails: { L: [{ S: 'detail' }, { N: '5' }] }, 48 | birthdays: { 49 | L: [ 50 | { 51 | M: { 52 | date: { S: new Date('1958-04-13').toISOString() }, 53 | presents: { 54 | L: [{ M: { description: { S: 'NHL voucher' } } }], 55 | }, 56 | }, 57 | }, 58 | ], 59 | }, 60 | awards: { 61 | L: [{ S: 'Best of Swiss Web' }], 62 | }, 63 | events: { 64 | L: [ 65 | { 66 | M: { 67 | name: { S: 'yearly get together' }, 68 | participants: { N: '125' }, 69 | }, 70 | }, 71 | ], 72 | }, 73 | emptySet: { SS: [] }, 74 | } 75 | -------------------------------------------------------------------------------- /test/data/product-dynamodb.data.ts: -------------------------------------------------------------------------------- 1 | import { Attributes } from '../../src/dynamo-easy' 2 | import { Product } from '../models' 3 | 4 | export const productFromDb: Attributes = { 5 | nestedValue: { 6 | M: { 7 | sortedSet: { 8 | L: [{ S: 'firstValue' }, { S: 'secondValue' }], 9 | }, 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /test/helper/get-meta-data-property.function.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata, PropertyMetadata } from '../../src/dynamo-easy' 2 | 3 | export function getMetaDataProperty( 4 | modelOptions: ModelMetadata, 5 | propertyKey: K, 6 | ): PropertyMetadata | undefined { 7 | return modelOptions.properties.find((property) => property.name === propertyKey) 8 | } 9 | -------------------------------------------------------------------------------- /test/helper/resetDynamoEasyConfig.function.ts: -------------------------------------------------------------------------------- 1 | import { updateDynamoEasyConfig } from '../../src/config/update-config.function' 2 | import { DEFAULT_SESSION_VALIDITY_ENSURER } from '../../src/dynamo/default-session-validity-ensurer.const' 3 | import { DEFAULT_TABLE_NAME_RESOLVER } from '../../src/dynamo/default-table-name-resolver.const' 4 | import { DEFAULT_LOG_RECEIVER } from '../../src/logger/default-log-receiver.const' 5 | import { dateToStringMapper } from '../../src/mapper/custom/date-to-string.mapper' 6 | 7 | export function resetDynamoEasyConfig() { 8 | updateDynamoEasyConfig({ 9 | dateMapper: dateToStringMapper, 10 | logReceiver: DEFAULT_LOG_RECEIVER, 11 | tableNameResolver: DEFAULT_TABLE_NAME_RESOLVER, 12 | sessionValidityEnsurer: DEFAULT_SESSION_VALIDITY_ENSURER, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /test/jest-setup.ts: -------------------------------------------------------------------------------- 1 | /* code in this file will be executed before all tests, but in the same context */ 2 | -------------------------------------------------------------------------------- /test/models/brutalist.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { CollectionProperty, DateProperty, Model, PartitionKey, Property } from '../../src/dynamo-easy' 3 | import { FormId, formIdMapper } from './real-world' 4 | 5 | @Model() 6 | export class BrutalistModelLevel4 { 7 | @DateProperty({ name: 'level4_date' }) 8 | level4Date: Date 9 | 10 | @Property({ name: 'level4_string' }) 11 | level4String: string 12 | 13 | level4Number: number 14 | 15 | @Property({ mapper: formIdMapper }) 16 | level4FormId: FormId 17 | 18 | @CollectionProperty({ itemMapper: formIdMapper, name: 'level4_set' }) 19 | level4Set: Set 20 | } 21 | 22 | @Model() 23 | export class BrutalistModelLevel3 { 24 | @CollectionProperty({ 25 | itemType: BrutalistModelLevel4, 26 | name: 'level3_list', 27 | sorted: true, 28 | }) 29 | level3Prop: Set 30 | } 31 | 32 | @Model() 33 | export class BrutalistModelLevel2 { 34 | @CollectionProperty({ 35 | itemType: BrutalistModelLevel3, 36 | name: 'level2_list', 37 | }) 38 | level2Prop: BrutalistModelLevel3[] 39 | } 40 | 41 | @Model() 42 | export class BrutalistModel { 43 | @PartitionKey() 44 | @Property({ mapper: formIdMapper }) 45 | id: FormId 46 | 47 | @CollectionProperty({ 48 | itemType: BrutalistModelLevel2, 49 | name: 'level1_list', 50 | }) 51 | level1Prop: BrutalistModelLevel2[] 52 | } 53 | -------------------------------------------------------------------------------- /test/models/char-array.mapper.ts: -------------------------------------------------------------------------------- 1 | import { MapperForType, StringSetAttribute } from '../../src/dynamo-easy' 2 | 3 | /** 4 | * stores a string as char array 5 | */ 6 | export const charArrayMapper: MapperForType = { 7 | toDb: (val) => ({ SS: val.split('') }), 8 | fromDb: (attr) => attr.SS.join(), 9 | } 10 | -------------------------------------------------------------------------------- /test/models/complex.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CollectionProperty, 3 | DateProperty, 4 | Model, 5 | PartitionKey, 6 | Property, 7 | SortKey, 8 | Transient, 9 | } from '../../src/dynamo-easy' 10 | import { NestedObject } from './nested-object.model' 11 | 12 | @Model({ tableName: 'complex_model' }) 13 | export class ComplexModel { 14 | @PartitionKey() 15 | id: string 16 | 17 | @SortKey() 18 | @DateProperty() 19 | creationDate: Date 20 | 21 | @DateProperty() 22 | lastUpdated: Date 23 | 24 | @Property({ name: 'isActive' }) 25 | active: boolean 26 | 27 | @CollectionProperty() 28 | set: Set 29 | 30 | /* 31 | * actually this value is always mapped to an array, so the typing is not correct, 32 | * we still leave it to check if it works 33 | */ 34 | @CollectionProperty({ sorted: true }) 35 | sortedSet: Set 36 | 37 | @CollectionProperty({ sorted: true, itemType: NestedObject }) 38 | sortedComplexSet: Set 39 | 40 | @Property() 41 | mapWithNoType: Map 42 | 43 | simpleProperty: number 44 | 45 | @Transient() 46 | transientField: string 47 | 48 | @Property({ name: 'my_nested_object' }) 49 | nestedObj: NestedObject 50 | } 51 | -------------------------------------------------------------------------------- /test/models/custom-table-name.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unnecessary-class 2 | 3 | import { Model } from '../../src/dynamo-easy' 4 | 5 | @Model({ tableName: 'myCustomName' }) 6 | export class CustomTableNameModel {} 7 | -------------------------------------------------------------------------------- /test/models/duration.model.ts: -------------------------------------------------------------------------------- 1 | import { MapperForType, NumberAttribute } from '../../src/dynamo-easy' 2 | 3 | export class Duration { 4 | value: number 5 | 6 | constructor(value: number) { 7 | this.value = value 8 | } 9 | 10 | addSeconds(val: number) { 11 | this.value += val 12 | } 13 | 14 | addMinutes(val: number) { 15 | this.value += val * 60 16 | } 17 | 18 | addHours(val: number) { 19 | this.value += val * 60 * 60 20 | } 21 | 22 | get asSeconds(): number { 23 | return this.value 24 | } 25 | 26 | get asMinutes(): number { 27 | return this.value / 60 28 | } 29 | 30 | get asHours(): number { 31 | return this.value / (60 * 60) 32 | } 33 | } 34 | 35 | export const durationMapper: MapperForType = { 36 | fromDb: (attr) => new Duration(parseInt(attr.N, 10)), 37 | toDb: (val) => ({ N: `${val.value}` }), 38 | } 39 | -------------------------------------------------------------------------------- /test/models/employee.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, DateProperty, Model } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class Employee { 5 | name: string 6 | 7 | age: number 8 | 9 | @DateProperty() 10 | createdAt: Date | null 11 | 12 | @CollectionProperty({ sorted: true }) 13 | sortedSet: Set 14 | 15 | constructor(name: string, age: number, createdAt: Date | null, sortedListValues: any[] | null) { 16 | this.name = name 17 | this.age = age 18 | this.createdAt = createdAt 19 | if (sortedListValues) { 20 | this.sortedSet = new Set(sortedListValues) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/models/fail-model.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { CollectionProperty, MapAttribute, MapperForType, Model, StringAttribute } from '../../src/dynamo-easy' 3 | 4 | const strangeMapper: MapperForType = { 5 | toDb: (propertyValue) => ({ M: { id: { S: `${propertyValue}` } } }), 6 | fromDb: (attributeValue) => ({ id: parseInt((attributeValue.M.id).S, 10) }), 7 | } 8 | 9 | class FailModelNestedFail { 10 | id: number 11 | } 12 | 13 | @Model() 14 | export class FailModel { 15 | // array <-> (S)et 16 | @CollectionProperty({ itemMapper: strangeMapper }) 17 | myFail: FailModelNestedFail[] 18 | } 19 | -------------------------------------------------------------------------------- /test/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './complex.model' 2 | export * from './custom-table-name.model' 3 | export * from './employee.model' 4 | export * from './model-with-default-value.model' 5 | export * from './model-with-custom-mapper.model' 6 | export * from './model-with-custom-mapper-for-sort-key.model' 7 | export * from './model-with-custom-mapper-and-default-value.model' 8 | export * from './model-with-date.model' 9 | export * from './model-with-enum.model' 10 | export * from './model-with-indexes.model' 11 | export * from './model-with-date-as-key.model' 12 | export * from './model-without-custom-mapper.model' 13 | export * from './model-without-partition-key.model' 14 | export * from './nested-complex.model' 15 | export * from './nested-object.model' 16 | export * from './organization.model' 17 | export * from './product.model' 18 | export * from './simple.model' 19 | export * from './simple-with-composite-partition-key.model' 20 | export * from './simple-with-partition-key.model' 21 | export * from './types.enum' 22 | export * from './update.model' 23 | -------------------------------------------------------------------------------- /test/models/model-with-collections.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, Model } from '../../src/dynamo-easy' 2 | import { NestedModelWithDate } from './nested-model-with-date.model' 3 | import { NestedObject } from './nested-object.model' 4 | import { FormId, formIdMapper } from './real-world' 5 | 6 | @Model() 7 | export class ModelWithCollections { 8 | // ================================================================ 9 | // should be mapped to (L)ist of (M)aps since itemType is complex 10 | @CollectionProperty({ itemType: NestedModelWithDate }) 11 | arrayOfNestedModelToList: NestedModelWithDate[] 12 | 13 | @CollectionProperty({ itemType: NestedModelWithDate }) 14 | setOfNestedModelToList: Set 15 | 16 | // ============================================================================== 17 | // should be mapped to (L)ist of (S)trings since it needs to preserve the order 18 | @CollectionProperty({ sorted: true, itemMapper: formIdMapper }) 19 | arrayOfFormIdToListWithStrings: FormId[] 20 | 21 | @CollectionProperty({ sorted: true, itemMapper: formIdMapper }) 22 | setOfFormIdToListWithStrings: Set 23 | 24 | // =========================================================================== 25 | // should be mapped to (L)ist of (M)aps since it complex type without mapper 26 | @CollectionProperty() 27 | arrayOfObjectsToList: NestedObject[] 28 | 29 | @CollectionProperty() 30 | setOfObjectsToList: Set 31 | 32 | // ==================================================================== 33 | // should be mapped to (String)(S)et since the itemMapper is provided 34 | @CollectionProperty({ itemMapper: formIdMapper }) 35 | arrayOfFormIdToSet: FormId[] 36 | 37 | @CollectionProperty({ itemMapper: formIdMapper }) 38 | setOfFormIdToSet: Set 39 | 40 | // should be mapped to List since it is an array 41 | @CollectionProperty() 42 | arrayOfStringToSet: string[] 43 | 44 | // should be mapped to Set 45 | @CollectionProperty() 46 | setOfStringToSet: Set 47 | } 48 | -------------------------------------------------------------------------------- /test/models/model-with-custom-mapper-and-default-value.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Model } from '../../src/decorator/impl/model/model.decorator' 3 | import { Property } from '../../src/decorator/impl/property/property.decorator' 4 | import { MapperForType } from '../../src/mapper/for-type/base.mapper' 5 | import { StringAttribute } from '../../src/mapper/type/attribute.type' 6 | 7 | export class MyProp { 8 | static default() { 9 | return new MyProp('default', 'none') 10 | } 11 | 12 | static parse(v: string) { 13 | const p = v.split('-') 14 | return new MyProp(p[0], p[1]) 15 | } 16 | 17 | static toString(v: MyProp) { 18 | return `${v.type}-${v.name}` 19 | } 20 | 21 | constructor(public type: string, public name: string) {} 22 | 23 | toString() { 24 | return MyProp.toString(this) 25 | } 26 | } 27 | 28 | const myPropMapper: MapperForType = { 29 | fromDb: a => MyProp.parse(a.S), 30 | toDb: v => ({ S: MyProp.toString(v) }), 31 | } 32 | 33 | @Model() 34 | export class ModelWithCustomMapperAndDefaultValue { 35 | @Property({ 36 | mapper: myPropMapper, 37 | defaultValueProvider: () => MyProp.default(), 38 | }) 39 | myProp: MyProp 40 | } 41 | -------------------------------------------------------------------------------- /test/models/model-with-custom-mapper-for-sort-key.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { MapperForType, Model, NumberAttribute, PartitionKey, Property, SortKey } from '../../src/dynamo-easy' 3 | 4 | export class CustomId { 5 | private static MULTIPLIER_E = 5 6 | 7 | date: Date 8 | 9 | id: number 10 | 11 | static parse(value: string): CustomId { 12 | const id = parseInt(value.substr(0, value.length - CustomId.MULTIPLIER_E), 10) 13 | const date = value.substr(value.length - CustomId.MULTIPLIER_E) 14 | 15 | const y = date.toString().substr(0, 4) 16 | const m = date.toString().substr(4, 2) 17 | const d = date.toString().substr(6, 2) 18 | 19 | return new CustomId(new Date(`${y}-${m}-${d}`), id) 20 | } 21 | 22 | static unparse(customId: CustomId): string { 23 | const yyyy = customId.date.getUTCFullYear() 24 | const mm = ((customId.date.getUTCMonth() + 1).toString()).padStart(2, '0') 25 | const dd = (customId.date.getUTCDate().toString()).padStart(2, '0') 26 | return `${yyyy}${mm}${dd}${(customId.id.toString()).padStart(CustomId.MULTIPLIER_E, '0')}` 27 | } 28 | 29 | constructor(date: Date, id: number) { 30 | this.date = date 31 | this.id = id 32 | } 33 | } 34 | 35 | export const CustomIdMapper: MapperForType = { 36 | fromDb: (attributeValue: NumberAttribute) => CustomId.parse(attributeValue.N), 37 | toDb: (propertyValue: CustomId) => ({ N: CustomId.unparse(propertyValue) }), 38 | } 39 | 40 | @Model() 41 | export class ModelWithCustomMapperForSortKeyModel { 42 | @PartitionKey() 43 | name: string 44 | 45 | @SortKey() 46 | @Property({ mapper: CustomIdMapper }) 47 | customId: CustomId 48 | 49 | constructor(name: string, id: CustomId) { 50 | this.name = name 51 | this.customId = id 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/models/model-with-custom-mapper.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | // tslint:disable:no-non-null-assertion 3 | 4 | import { MapperForType, Model, PartitionKey, Property, StringAttribute } from '../../src/dynamo-easy' 5 | 6 | export class Id { 7 | counter: number 8 | year: number 9 | 10 | static parse(idString: string): Id { 11 | const id: Id = new Id() 12 | id.counter = parseInt(idString.slice(0, 4).replace('0', ''), 10) 13 | id.year = parseInt(idString.slice(4, 8), 10) 14 | return id 15 | } 16 | 17 | static unparse(propertyValue: Id): string { 18 | // create leading zeroes so the counter matches the pattern /d{4} 19 | const leadingZeroes: string = new Array(4 + 1 - (propertyValue.counter + '').length).join('0') 20 | return `${leadingZeroes}${propertyValue.counter}${propertyValue.year}` 21 | } 22 | 23 | constructor(counter?: number, year?: number) { 24 | this.counter = counter! 25 | this.year = year! 26 | } 27 | } 28 | 29 | export const IdMapper: MapperForType = { 30 | fromDb: (attributeValue: StringAttribute) => Id.parse(attributeValue.S), 31 | toDb: (propertyValue: Id) => ({ S: `${Id.unparse(propertyValue)}` }), 32 | } 33 | 34 | @Model() 35 | export class ModelWithCustomMapperModel { 36 | @Property({ mapper: IdMapper }) 37 | @PartitionKey() 38 | id: Id 39 | } 40 | -------------------------------------------------------------------------------- /test/models/model-with-date-as-key.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { DateProperty, GSIPartitionKey, Model, PartitionKey, SortKey } from '../../src/dynamo-easy' 3 | 4 | @Model() 5 | export class ModelWithDateAsHashKey { 6 | @PartitionKey() 7 | @DateProperty() 8 | startDate: Date 9 | 10 | constructor(startDate: Date) { 11 | this.startDate = startDate 12 | } 13 | } 14 | 15 | @Model() 16 | export class ModelWithDateAsRangeKey { 17 | @PartitionKey() 18 | id: number 19 | 20 | @SortKey() 21 | @DateProperty() 22 | creationDate: Date 23 | 24 | constructor(id: number, creationDate: Date) { 25 | this.id = id 26 | this.creationDate = creationDate 27 | } 28 | } 29 | 30 | @Model() 31 | export class ModelWithDateAsIndexHashKey { 32 | @PartitionKey() 33 | id: number 34 | 35 | @GSIPartitionKey('anyGSI') 36 | @DateProperty() 37 | creationDate: Date 38 | 39 | constructor(id: number, creationDate: Date) { 40 | this.id = id 41 | this.creationDate = creationDate 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/models/model-with-date.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, SortKey } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class ModelWithDate { 5 | @PartitionKey() 6 | id: string 7 | 8 | @SortKey() 9 | creationDate: Date 10 | } 11 | -------------------------------------------------------------------------------- /test/models/model-with-default-value.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, Property } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class ModelWithDefaultValue { 5 | @PartitionKey() 6 | @Property({ defaultValueProvider: () => `generated-id-${Math.floor(Math.random() * 1000)}` }) 7 | id: string 8 | } 9 | -------------------------------------------------------------------------------- /test/models/model-with-empty-values.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class ModelWithEmptyValues { 5 | @PartitionKey() 6 | id: string 7 | 8 | name: string 9 | 10 | roles: Set 11 | 12 | createdAt: Date | null 13 | 14 | lastNames: string[] 15 | 16 | details: { info?: string } 17 | } 18 | -------------------------------------------------------------------------------- /test/models/model-with-enum.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, Property } from '../../src/dynamo-easy' 2 | import { StringType, Type } from './types.enum' 3 | 4 | @Model() 5 | export class ModelWithEnum { 6 | @PartitionKey() 7 | id: string 8 | 9 | @Property() 10 | type: Type 11 | 12 | @Property() 13 | strType: StringType 14 | } 15 | 16 | @Model() 17 | export class ModelWithNonDecoratedEnum { 18 | @PartitionKey() 19 | id: string 20 | 21 | type: Type 22 | 23 | strType: StringType 24 | } 25 | -------------------------------------------------------------------------------- /test/models/model-with-indexes.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { 3 | DateProperty, 4 | GSIPartitionKey, 5 | GSISortKey, 6 | LSISortKey, 7 | Model, 8 | PartitionKey, 9 | Property, 10 | SortKey, 11 | } from '../../src/dynamo-easy' 12 | 13 | export const INDEX_ACTIVE = 'active-index' 14 | 15 | @Model() 16 | export class ModelWithGSI { 17 | @PartitionKey() 18 | id: string 19 | 20 | @DateProperty() 21 | createdAt: Date 22 | 23 | @GSIPartitionKey(INDEX_ACTIVE) 24 | active: boolean 25 | } 26 | 27 | @Model() 28 | export class ModelWithLSI { 29 | @PartitionKey() 30 | id: string 31 | 32 | @DateProperty() 33 | createdAt: Date 34 | 35 | @LSISortKey(INDEX_ACTIVE) 36 | active: boolean 37 | } 38 | 39 | export const INDEX_COUNT = 'count-index' 40 | export const INDEX_ACTIVE_CREATED_AT = 'active-createdAt-index' 41 | 42 | @Model() 43 | export class ModelWithABunchOfIndexes { 44 | @Property({ name: 'myId' }) 45 | @PartitionKey() 46 | id: string 47 | 48 | @SortKey() 49 | @GSISortKey(INDEX_ACTIVE_CREATED_AT) 50 | @DateProperty() 51 | createdAt: Date 52 | 53 | @GSIPartitionKey(INDEX_ACTIVE_CREATED_AT) 54 | active: boolean 55 | 56 | @LSISortKey(INDEX_COUNT) 57 | count: number 58 | } 59 | 60 | @Model() 61 | export class DifferentModel { 62 | @PartitionKey() 63 | id: string 64 | 65 | @GSISortKey(INDEX_ACTIVE) 66 | createdAt: boolean 67 | 68 | @GSIPartitionKey(INDEX_ACTIVE) 69 | active: boolean 70 | } 71 | 72 | @Model() 73 | export class ModelWithWrongIndexes { 74 | @PartitionKey() 75 | id: string 76 | 77 | @GSISortKey(INDEX_ACTIVE) 78 | createdAt: boolean 79 | 80 | @GSIPartitionKey(INDEX_ACTIVE) 81 | active: boolean 82 | 83 | // @GSISortKey(INDEX_ACTIVE) 84 | otherField: string 85 | } 86 | -------------------------------------------------------------------------------- /test/models/model-with-nested-model-with-custom-mapper.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Model, Property } from '../../src/dynamo-easy' 3 | import { Id, IdMapper } from './model-with-custom-mapper.model' 4 | 5 | @Model() 6 | export class NestedModelWithCustomMapper { 7 | @Property({ mapper: IdMapper }) 8 | id: Id 9 | 10 | constructor() { 11 | this.id = new Id(9, 2010) 12 | } 13 | } 14 | 15 | @Model() 16 | export class ModelWithNestedModelWithCustomMapper { 17 | @Property() 18 | nestedModel: NestedModelWithCustomMapper 19 | 20 | constructor() { 21 | this.nestedModel = new NestedModelWithCustomMapper() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/models/model-without-custom-mapper.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { GSIPartitionKey, Model, PartitionKey } from '../../src/dynamo-easy' 3 | 4 | @Model() 5 | export class ModelWithoutCustomMapper { 6 | @PartitionKey() 7 | id: { key: string; value: string } 8 | 9 | otherVal: string 10 | 11 | constructor(key: string, value: string, otherValue: string) { 12 | this.id = { key, value } 13 | this.otherVal = otherValue 14 | } 15 | } 16 | 17 | @Model() 18 | export class ModelWithoutCustomMapperOnIndex { 19 | @PartitionKey() 20 | id: string 21 | 22 | @GSIPartitionKey('anyGSI') 23 | gsiPk: { key: string; value: string } 24 | 25 | constructor(id: string, key: string, value: string) { 26 | this.id = id 27 | this.gsiPk = { key, value } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/models/model-without-partition-key.model.ts: -------------------------------------------------------------------------------- 1 | import { GSISortKey } from '../../src/decorator/impl/index/gsi-sort-key.decorator' 2 | import { Model } from '../../src/decorator/impl/model/model.decorator' 3 | import { Property } from '../../src/decorator/impl/property/property.decorator' 4 | 5 | export const FAIL_MODEL_GSI = 'failModelGsi' 6 | 7 | @Model() 8 | export class ModelWithoutPartitionKeyModel { 9 | @Property() 10 | name: string 11 | 12 | @GSISortKey(FAIL_MODEL_GSI) 13 | gsiRange: string 14 | } 15 | -------------------------------------------------------------------------------- /test/models/nested-complex.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, Model } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class NestedComplexModel { 5 | // should be mapped to a L DynamoDb Type to preserve the order 6 | @CollectionProperty({ sorted: true }) 7 | sortedSet: Set 8 | 9 | constructor() { 10 | this.sortedSet = new Set(['firstValue', 'secondeValue']) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/models/nested-model-with-date.model.ts: -------------------------------------------------------------------------------- 1 | import { DateProperty, Model } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class NestedModelWithDate { 5 | @DateProperty() 6 | updated: Date 7 | } 8 | -------------------------------------------------------------------------------- /test/models/nested-object.model.ts: -------------------------------------------------------------------------------- 1 | import { DateProperty, Model } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class NestedObject { 5 | id: string 6 | 7 | @DateProperty({ name: 'my_date' }) 8 | date?: Date 9 | } 10 | -------------------------------------------------------------------------------- /test/models/product.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { CollectionProperty, Model, Property } from '../../src/dynamo-easy' 3 | import { NestedComplexModel } from './nested-complex.model' 4 | 5 | @Model() 6 | export class ProductNested { 7 | @CollectionProperty({ sorted: true }) 8 | collection: Set 9 | 10 | counter = 0 11 | 12 | constructor() { 13 | this.collection = new Set() 14 | for (let i = 0; i < 3; i++) { 15 | this.collection.add(`value${++this.counter}`) 16 | } 17 | } 18 | } 19 | 20 | @Model() 21 | export class Product { 22 | @Property() 23 | nestedValue: NestedComplexModel 24 | 25 | @CollectionProperty({ itemType: ProductNested }) 26 | list: ProductNested[] 27 | 28 | constructor() { 29 | this.nestedValue = new NestedComplexModel() 30 | this.list = [] 31 | this.list.push(new ProductNested()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/models/real-world/base-form.model.ts: -------------------------------------------------------------------------------- 1 | import { DateProperty, GSIPartitionKey, Model, PartitionKey } from '../../../src/dynamo-easy' 2 | 3 | export const INDEX_CREATION_DATE = 'index-creationDate' 4 | 5 | @Model({ tableName: 'forms' }) 6 | export class BaseForm { 7 | @PartitionKey() 8 | id: string 9 | 10 | @GSIPartitionKey(INDEX_CREATION_DATE) 11 | @DateProperty() 12 | creationDate: Date 13 | 14 | @DateProperty() 15 | lastSavedDate: Date 16 | } 17 | -------------------------------------------------------------------------------- /test/models/real-world/extended-form.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, Property } from '../../../src/dynamo-easy' 2 | import { Form } from './form.model' 3 | 4 | @Model({ tableName: 'forms' }) 5 | export class ExtendedFormModel extends Form { 6 | @Property() 7 | myOtherProperty: string 8 | } 9 | -------------------------------------------------------------------------------- /test/models/real-world/form-type.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * these types are used to generate forms linked to a product: 3 | * - request 4 | * - quote 5 | * 6 | * frame-order is another container where all the frame-order forms are linked to 7 | * - frame-order 8 | * 9 | * and these to link to an order: 10 | * - order 11 | * - confirmation 12 | * - delivery 13 | * - invoice 14 | * - warning 15 | * - credit 16 | * - debit 17 | * - cover 18 | * - palette info 19 | */ 20 | 21 | // NOTE we persist the index of formType, so NEVER change the index and always add one with new types 22 | export enum FormType { 23 | REQUEST = 0, 24 | QUOTE = 1, 25 | ORDER = 2, 26 | CONFIRMATION = 3, 27 | DELIVERY = 4, 28 | INVOICE = 5, 29 | CREDIT = 6, 30 | DEBIT = 7, 31 | FAILURE_RETURN = 8, 32 | COVER = 9, 33 | PALETTE_INFO = 10, 34 | FRAME_ORDER = 11, 35 | WARNING = 12, 36 | INVOICE_GMBH = 13, 37 | STOCK_ORDER = 14, 38 | STOCK_COVER = 15, 39 | } 40 | -------------------------------------------------------------------------------- /test/models/real-world/form.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, Model } from '../../../src/dynamo-easy' 2 | import { BaseForm } from './base-form.model' 3 | 4 | @Model({ tableName: 'forms' }) 5 | export class Form extends BaseForm { 6 | @CollectionProperty({ sorted: true }) 7 | types: number[] 8 | } 9 | -------------------------------------------------------------------------------- /test/models/real-world/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-form.model' 2 | export * from './extended-form.model' 3 | export * from './form.model' 4 | export * from './form-id.model' 5 | export * from './form-type.enum' 6 | export * from './order.model' 7 | export * from './order-id.model' 8 | export * from './product-base-form.model' 9 | -------------------------------------------------------------------------------- /test/models/real-world/order-id.model.ts: -------------------------------------------------------------------------------- 1 | import { MapperForType, StringAttribute } from '../../../src/dynamo-easy' 2 | 3 | export class OrderId { 4 | counter: number 5 | year: number 6 | 7 | static parse(orderId?: string): OrderId { 8 | if (orderId) { 9 | const counter: number = parseInt(orderId.slice(0, 4).replace('0', ''), 10) 10 | const year: number = parseInt(orderId.slice(4), 10) 11 | 12 | return new OrderId(counter, year) 13 | } else { 14 | throw new Error('orderId must be provided as string, got no value') 15 | } 16 | } 17 | 18 | static unparse(formId: OrderId): string { 19 | // use the join method with array length to produce leading zeroes 20 | const leadingZeroes: string = new Array(4 + 1 - (formId.counter + '').length).join('0') 21 | return leadingZeroes + formId.counter + formId.year 22 | } 23 | 24 | constructor(counter: number, year: number) { 25 | this.counter = counter 26 | this.year = year 27 | } 28 | } 29 | 30 | export const orderIdMapper: MapperForType = { 31 | fromDb: (attributeValue: StringAttribute) => OrderId.parse(attributeValue.S), 32 | toDb: (propertyValue: OrderId) => ({ S: OrderId.unparse(propertyValue) }), 33 | } 34 | -------------------------------------------------------------------------------- /test/models/real-world/order.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | 3 | import { 4 | CollectionProperty, 5 | DateProperty, 6 | GSIPartitionKey, 7 | GSISortKey, 8 | Model, 9 | PartitionKey, 10 | Property, 11 | Transient, 12 | } from '../../../src/dynamo-easy' 13 | import { FormId, formIdMapper } from './form-id.model' 14 | import { FormType } from './form-type.enum' 15 | import { OrderId, orderIdMapper } from './order-id.model' 16 | 17 | @Model() 18 | export class BaseOrder { 19 | @PartitionKey() 20 | @Property({ mapper: orderIdMapper }) 21 | id: OrderId 22 | 23 | @GSIPartitionKey('order_product_id_creation_date') 24 | productId: string 25 | 26 | @GSISortKey('order_product_id_creation_date') 27 | @DateProperty() 28 | creationDate: Date 29 | 30 | @CollectionProperty({ sorted: true, itemMapper: formIdMapper }) // mapped to list, since sorted 31 | formIds: FormId[] 32 | 33 | // internal use for UI only, should not be persisted 34 | @Transient() 35 | isNew?: boolean 36 | } 37 | 38 | @Model() 39 | export class Order extends BaseOrder { 40 | // should map to number enum 41 | types: FormType[] 42 | 43 | displayOrderCustomerIdentNr: string 44 | 45 | orderCustomerIdentNr: string // lowercase for search 46 | } 47 | -------------------------------------------------------------------------------- /test/models/real-world/product-base-form.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, SortKey } from '../../../src/dynamo-easy' 2 | import { BaseForm } from './base-form.model' 3 | 4 | @Model({ tableName: 'forms' }) 5 | export class ProductBaseFormModel extends BaseForm { 6 | @SortKey() 7 | productId: string 8 | } 9 | -------------------------------------------------------------------------------- /test/models/simple-with-composite-partition-key.model.ts: -------------------------------------------------------------------------------- 1 | import { DateProperty, Model, PartitionKey, Property, SortKey } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class SimpleWithCompositePartitionKeyModel { 5 | @PartitionKey() 6 | id: string 7 | 8 | @SortKey() 9 | @DateProperty() 10 | creationDate: Date 11 | 12 | age: number 13 | } 14 | 15 | @Model() 16 | export class SimpleWithRenamedCompositePartitionKeyModel { 17 | @PartitionKey() 18 | @Property({ name: 'custom_id' }) 19 | id: string 20 | 21 | @SortKey() 22 | @DateProperty({ name: 'custom_date' }) 23 | creationDate: Date 24 | 25 | age: number 26 | } 27 | -------------------------------------------------------------------------------- /test/models/simple-with-partition-key.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, Property } from '../../src/dynamo-easy' 2 | 3 | @Model() 4 | export class SimpleWithPartitionKeyModel { 5 | @PartitionKey() 6 | id: string 7 | 8 | age: number 9 | } 10 | 11 | @Model() 12 | export class SimpleWithRenamedPartitionKeyModel { 13 | @PartitionKey() 14 | @Property({ name: 'custom_id' }) 15 | id: string 16 | 17 | age: number 18 | } 19 | -------------------------------------------------------------------------------- /test/models/simple.model.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-unnecessary-class 2 | import { Model } from '../../src/dynamo-easy' 3 | 4 | @Model() 5 | export class SimpleModel {} 6 | -------------------------------------------------------------------------------- /test/models/special-cases-model.model.ts: -------------------------------------------------------------------------------- 1 | import { Model, PartitionKey, Property } from '../../src/dynamo-easy' 2 | import { charArrayMapper } from './char-array.mapper' 3 | import { Duration, durationMapper } from './duration.model' 4 | 5 | @Model() 6 | export class SpecialCasesModel { 7 | @PartitionKey() 8 | id: string 9 | 10 | @Property({ mapper: charArrayMapper }) 11 | myChars: string 12 | 13 | @Property({ mapper: durationMapper }) 14 | duration: Duration 15 | } 16 | -------------------------------------------------------------------------------- /test/models/types.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Type { 2 | FirstType, 3 | SecondType, 4 | ThirdType, 5 | } 6 | 7 | export enum StringType { 8 | FirstType = 'first', 9 | SecondType = 'second', 10 | } 11 | -------------------------------------------------------------------------------- /test/models/update.model.ts: -------------------------------------------------------------------------------- 1 | import { CollectionProperty, DateProperty, Model, PartitionKey, Property } from '../../src/dynamo-easy' 2 | 3 | // tslint:disable-next-line:max-classes-per-file 4 | @Model() 5 | export class Address { 6 | street: string 7 | place: string 8 | zip: number 9 | } 10 | 11 | // tslint:disable-next-line:max-classes-per-file 12 | @Model() 13 | export class Info { 14 | details: string 15 | 16 | @DateProperty() 17 | createdAt: Date 18 | } 19 | 20 | // tslint:disable-next-line:max-classes-per-file 21 | @Model() 22 | export class UpdateModel { 23 | @PartitionKey() 24 | id: string 25 | 26 | @DateProperty() 27 | creationDate: Date 28 | 29 | @DateProperty() 30 | lastUpdated: Date 31 | 32 | name: string 33 | 34 | @Property({ name: 'isActive' }) 35 | active: boolean 36 | 37 | counter: number 38 | 39 | // maps to L(ist) 40 | addresses: Address[] 41 | 42 | @CollectionProperty({ sorted: true }) 43 | numberValues: number[] 44 | 45 | @CollectionProperty({ itemType: Info }) 46 | informations: Info[] 47 | 48 | // maps to M(ap) 49 | info: Info 50 | 51 | // maps to S(tring)S(et) 52 | topics: Set 53 | } 54 | -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require('shelljs') 2 | const { readFileSync } = require('fs') 3 | const url = require('url') 4 | 5 | const info = { 6 | TRAVIS_BRANCH: process.env.TRAVIS_BRANCH, 7 | TRAVIS_PULL_REQUEST: process.env.TRAVIS_PULL_REQUEST, 8 | TRAVIS_PULL_REQUEST_BRANCH: process.env.TRAVIS_PULL_REQUEST_BRANCH, 9 | } 10 | echo(`running on branch ${JSON.stringify(info)}`) 11 | 12 | if (info.TRAVIS_BRANCH === 'master' && info.TRAVIS_PULL_REQUEST === 'false') { 13 | const pkg = JSON.parse(readFileSync('package.json')) 14 | let repoUrl 15 | if (typeof pkg.repository === 'object') { 16 | if (!pkg.repository.hasOwnProperty('url')) { 17 | throw new Error('URL does not exist in repository section') 18 | } 19 | repoUrl = pkg.repository.url 20 | } else { 21 | repoUrl = pkg.repository 22 | } 23 | 24 | const parsedUrl = url.parse(repoUrl) 25 | const repository = (parsedUrl.host || '') + (parsedUrl.path || '') 26 | const ghToken = process.env.GH_TOKEN 27 | 28 | echo('Deploying docs!!!') 29 | cd('dist/docs') 30 | touch('.nojekyll') 31 | exec('git init') 32 | exec('git add .') 33 | exec('git config user.name "Michael Wittwer"') 34 | exec('git config user.email "michael.wittwer@shiftcode.ch"') 35 | exec('git commit -m "docs(docs): update gh-pages"') 36 | exec( 37 | `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages` 38 | ) 39 | echo('Docs deployed!!') 40 | } else { 41 | echo('Not running on master -> skipping docs deployment') 42 | } 43 | -------------------------------------------------------------------------------- /tools/tslint/noDynamoNamedImportRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import * as Lint from 'tslint' 3 | import { isNamedImports } from 'tsutils' 4 | 5 | /** 6 | * We prevent named imports from aws-sdk/clients/dynamodb, this is a design decision to be more obvious about where the 7 | * import is from, this is not common practice but because our code has a lot of code dependent on dynamoDB we do this 8 | * for easier reading and understanding 9 | */ 10 | class NoDynamoDbWildcardImportWalker extends Lint.RuleWalker { 11 | 12 | visitImportDeclaration(node: ts.ImportDeclaration) { 13 | const moduleName = node.moduleSpecifier.getText(this.getSourceFile()) 14 | // remove outer quotes string looks like "'moduleName'" 15 | .replace(/"|'/g, '') 16 | if (moduleName === 'aws-sdk/clients/dynamodb' && isNamedImports(node.importClause.namedBindings)) { 17 | this.addFailureAtNode(node, Rule.FAILURE_STRING) 18 | } 19 | 20 | // call the base version of this visitor to actually parse this node 21 | super.visitImportDeclaration(node) 22 | } 23 | } 24 | 25 | export class Rule extends Lint.Rules.AbstractRule { 26 | static FAILURE_STRING = 'only wildcard import (import * as DynamoDB from \'aws-sdk/clients/dynamodb\') is allowed for this module' 27 | 28 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 29 | return this.applyWithWalker(new NoDynamoDbWildcardImportWalker(sourceFile, this.getOptions())) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tools/tslint/test/test.ts.lint: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment' 2 | import * as DynamoDB from 'aws-sdk' 3 | import { Config } from 'aws-sdk' 4 | import { Key } from 'aws-sdk/clients/dynamodb' 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [only wildcard import (import * as DynamoDB from 'aws-sdk/clients/dynamodb') is allowed for this module] 6 | -------------------------------------------------------------------------------- /tools/tslint/test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "../" 4 | ], 5 | "rules": { 6 | "no-dynamo-named-import": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, /* Generates corresponding '.d.ts' file. */ 5 | "types": [ /* Type declaration files to be included in compilation. */ 6 | "node", 7 | "reflect-metadata", 8 | "jest" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "test/**/*" 14 | ], 15 | "exclude": [] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "tslint:latest" 3 | - "tslint-config-prettier" 4 | rulesDirectory: 5 | - ./tools/tslint 6 | rules: 7 | # 8 | # override rules from tslint:recommended 9 | # 10 | interface-name: [true, "never-prefix"] 11 | member-access: [true, "no-public"] # true 12 | member-ordering: [ # order: "statics-first" 13 | true, 14 | { 15 | order: [ 16 | "public-static-field", 17 | "private-static-field", 18 | "public-instance-field", 19 | "private-instance-field", 20 | "public-static-method", 21 | "private-static-method", 22 | "constructor", 23 | "public-instance-method", 24 | "private-instance-method", 25 | ] 26 | } 27 | ] 28 | no-angle-bracket-type-assertion: false # true - use «» instead of «as Type» for casting 29 | no-console: [false, "debug", "warn", "error"] # true 30 | object-literal-sort-keys: false 31 | quotemark: [true, "single", "avoid-escape"] # ["double", "avoid-escape"] 32 | triple-equals: [true, "allow-null-check"] # true - allow for != null check to catch null & undefined 33 | 34 | # 35 | # override rules from tslint:latest 36 | # 37 | no-object-literal-type-assertion: false 38 | no-submodule-imports: [true, "aws-sdk", "rxjs/operators"] 39 | prefer-conditional-expression: false 40 | 41 | # 42 | # additional rules 43 | # 44 | no-inferrable-types: [true, "ignore-params"] 45 | no-non-null-assertion: true 46 | no-unnecessary-callback-wrapper: true 47 | no-unnecessary-class: true 48 | no-unnecessary-type-assertion: true 49 | no-unused-variable: true 50 | import-blacklist: 51 | - true 52 | - aws-sdk 53 | # introduced with v5.12.0 54 | unnecessary-constructor: true 55 | unnecessary-bind: true 56 | 57 | # 58 | # custom rules 59 | # 60 | no-dynamo-named-import: true 61 | --------------------------------------------------------------------------------