├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── SECURITY.md ├── __test__ ├── Condition.test.ts ├── ExpressionAttributes.test.ts ├── Fields.test.ts ├── FieldsAdvanced.test.ts ├── KeyCondition.test.ts ├── Model.test.ts ├── Table.test.ts ├── TableIndex.test.ts ├── TableValidate.test.ts ├── Update.test.ts ├── index.test.ts └── testCommon.ts ├── api-extractor.json ├── dependencygraph.svg ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.css │ ├── icons.png │ ├── icons@2x.png │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ ├── Fields.FieldBase-1.html │ ├── Table.PrimaryKey-1.html │ ├── condition.html │ ├── conditionexpression.html │ ├── expressionattributes.html │ ├── fields.fieldbinary.html │ ├── fields.fieldbinaryset.html │ ├── fields.fieldboolean.html │ ├── fields.fieldcomposite.html │ ├── fields.fieldcompositenamed.html │ ├── fields.fieldcompositeslot.html │ ├── fields.fieldcreateddate.html │ ├── fields.fieldcreatednumberdate.html │ ├── fields.fielddate.html │ ├── fields.fieldexpression.html │ ├── fields.fieldhidden.html │ ├── fields.fieldlist.html │ ├── fields.fieldmap.html │ ├── fields.fieldmodel.html │ ├── fields.fieldmodellist.html │ ├── fields.fieldmodelmap.html │ ├── fields.fieldnull.html │ ├── fields.fieldnumber.html │ ├── fields.fieldnumberset.html │ ├── fields.fieldrevision.html │ ├── fields.fieldset.html │ ├── fields.fieldsplit.html │ ├── fields.fieldstring.html │ ├── fields.fieldstringset.html │ ├── fields.fieldtype.html │ ├── fields.fieldupdateddate.html │ ├── fields.fieldupdatednumberdate.html │ ├── fields.html │ ├── index.html │ ├── keycondition.html │ ├── keyconditionexpression.html │ ├── model.html │ ├── model.modelresult.html │ ├── table.batchget.html │ ├── table.batchwrite.html │ ├── table.html │ ├── table.transactget.html │ ├── table.transactwrite.html │ ├── update.html │ └── updateexpression.html ├── index.html ├── interfaces │ ├── condition.expression.html │ ├── fields.attributedefinition.html │ ├── fields.baseoptions.html │ ├── fields.compositenamedoptions.html │ ├── fields.compositeoptions.html │ ├── fields.createddateoptions.html │ ├── fields.field.html │ ├── fields.modelcontext.html │ ├── fields.modellistoptions.html │ ├── fields.modelmapoptions.html │ ├── fields.modeloptions.html │ ├── fields.revisionoptions.html │ ├── fields.setoptions.html │ ├── fields.splitoptions.html │ ├── fields.tablecontext.html │ ├── fields.typeoptions.html │ ├── fields.updatedateoptions.html │ ├── fields.updatenumberdateoptions.html │ ├── index.defaultglobalindexkey.html │ ├── index.defaultlocalindexkey.html │ ├── index.indexparams.html │ ├── index.indexparamst.html │ ├── index.indext.html │ ├── keycondition.expression.html │ ├── model.baseoutput.html │ ├── model.deleteoutput.html │ ├── model.getoutput.html │ ├── model.modelbase.html │ ├── model.modelparams.html │ ├── model.modelparamst.html │ ├── model.modelresultt.html │ ├── model.modelt.html │ ├── model.putoutput.html │ ├── model.tabledata.html │ ├── model.tableupdatedata.html │ ├── model.updateoutput.html │ ├── table.addparamsoptions.html │ ├── table.baseoptions.html │ ├── table.batchgetoptions.html │ ├── table.batchgettableinput.html │ ├── table.batchgettableoptions.html │ ├── table.batchwritetableoptions.html │ ├── table.defaulttablekey.html │ ├── table.deleteinput.html │ ├── table.deleteoptions.html │ ├── table.expressionattributes.html │ ├── table.getinput.html │ ├── table.getoptions.html │ ├── table.primarykey.keyquery.html │ ├── table.putinput.html │ ├── table.putitem.html │ ├── table.putitemt.html │ ├── table.putoptions.html │ ├── table.queryinput.html │ ├── table.queryoptions.html │ ├── table.scaninput.html │ ├── table.scanoptions.html │ ├── table.tableparams.html │ ├── table.tableparamst.html │ ├── table.tableresult.html │ ├── table.tablet.html │ ├── table.transactgetitem.html │ ├── table.transactgetitemt.html │ ├── table.transactgettableoptions.html │ ├── table.transactwritedata.html │ ├── table.transactwritedatat.html │ ├── table.transactwritetableoptions.html │ ├── table.updateinput.html │ ├── table.updateoptions.html │ ├── table.writeoptions.html │ └── update.expression.html ├── modules.html └── modules │ ├── Condition.html │ ├── Fields.FieldBase.html │ ├── Fields.html │ ├── Index.html │ ├── KeyCondition.html │ ├── Model.html │ ├── Table.PrimaryKey.html │ ├── Table.html │ └── Update.html ├── etc ├── dynamodb-datamodel.api.md └── readme.md ├── examples ├── Condition.ts ├── Fields.composite.ts ├── Fields.compositeNamed.ts ├── Fields.split.ts ├── Fields.ts ├── Fields.type.ts ├── Index.ts ├── KeyCondition.ts ├── Model.ts ├── Readme.BasicUsage.ts ├── Readme.Condition.Fields.ts ├── Readme.Condition.ts ├── Table.Simple.ts ├── Table.ts ├── Update.Model.ts ├── Update.Table.ts ├── baseUrl │ └── dynamodb-datamodel.ts ├── javascript │ ├── Fields.compositeNamed.js │ ├── Index.js │ ├── Models.js │ ├── Table.Simple.js │ ├── Table.js │ └── test │ │ ├── Fields.Examples.test.js │ │ ├── Index.Examples.test.ts │ │ ├── Model.examples.test.js │ │ └── Table.Examples.test.js ├── readme.md └── test │ ├── Condition.test.ts │ ├── Fields.Examples.test.ts │ ├── Index.Examples.test.ts │ ├── KeyCondition.test.ts │ ├── Model.Examples.test.ts │ ├── Readme.Examples.test.ts │ ├── Table.Examples.test.ts │ └── Update.Examples.test.ts ├── inch.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── Condition.ts ├── ExpressionAttributes.ts ├── Fields.ts ├── KeyCondition.ts ├── Model.ts ├── Table.ts ├── TableIndex.ts ├── TableValidate.ts ├── Update.ts └── index.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.src.esm.json ├── tsconfig.src.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | docs -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | root: true, 16 | env: { 17 | es6: true, 18 | node: true, 19 | }, 20 | extends: [ 21 | 'eslint:recommended', 22 | 'plugin:@typescript-eslint/eslint-recommended', 23 | 'plugin:@typescript-eslint/recommended', 24 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 25 | 'plugin:jest/recommended', 26 | 'plugin:prettier/recommended', 27 | ], 28 | parser: '@typescript-eslint/parser', 29 | parserOptions: { 30 | project: './tsconfig.eslint.json', 31 | sourceType: 'module', 32 | projectFolderIgnoreList: ['/node_modules/', '/docs/', '/dist/'], 33 | ecmaVersion: 6, 34 | }, 35 | plugins: ['@typescript-eslint', 'jest', 'tsdoc'], 36 | rules: { 37 | 'jest/no-standalone-expect': 'off', // Disable no-standalone-expect so that examples are self contained 38 | 'tsdoc/syntax': 'error', 39 | '@typescript-eslint/no-unused-vars': 'error', 40 | '@typescript-eslint/no-require-imports': 'error', 41 | 'no-console': 'error', 42 | 'no-alert': 'error', 43 | 'no-caller': 'error', 44 | 'no-constructor-return': 'error', 45 | eqeqeq: 'error', 46 | }, 47 | overrides: [ 48 | { 49 | // add limited set of rules here to avoid having to add eslint-disable comments to examples 50 | files: ['examples/**/*.js'], 51 | rules: { 52 | '@typescript-eslint/no-var-requires': 'off', 53 | '@typescript-eslint/no-require-imports': 'off', 54 | '@typescript-eslint/explicit-function-return-type': 'off', 55 | }, 56 | }, 57 | { 58 | // add limited set of rules here to avoid having to add eslint-disable comments to examples 59 | files: ['examples/**/*.ts'], 60 | rules: { 61 | '@typescript-eslint/unbound-method': 'off', 62 | }, 63 | }, 64 | ], 65 | }; 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: JasonCraftsCode 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Sample code that reproduce behavior** 13 | Link to sample code or add sample code to code block below. 14 | 15 | ```javascript 16 | ``` 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Environment (please complete the following information):** 22 | 23 | - Node.js version: 24 | - AWS-SDK version: 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.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 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when ... 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | **Test Configuration**: 24 | 25 | - Node.js version: 26 | - AWS-SDK version: 27 | 28 | ## Checklist 29 | 30 | - [ ] My code follows the style guidelines of this project 31 | - [ ] I have performed a self-review of my own code 32 | - [ ] I have commented my code, particularly in hard-to-understand areas 33 | - [ ] I have made corresponding changes to the documentation 34 | - [ ] My changes generate no new warnings 35 | - [ ] I have added tests that prove my fix is effective or that my feature works 36 | - [ ] New and existing unit tests pass locally with my changes 37 | - [ ] Any dependent changes have been merged and published in downstream modules 38 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [main, master] 9 | pull_request: 10 | branches: [main, master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | node-version: [12.x, 14.x] 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - run: node --version 31 | - run: npm --version 32 | 33 | - name: Install npm dependencies and build 34 | run: npm ci 35 | 36 | - name: Run lint 37 | run: npm run lint 38 | 39 | - name: Run tests under code coverage 40 | run: npm run test:coverage 41 | env: 42 | CI: true 43 | 44 | - name: Upload coverage to Codecov 45 | uses: codecov/codecov-action@v1 46 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '26 13 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | temp -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonCraftsCode/dynamodb-datamodel/2b3900d909db5838687af6cb636fadec052ec5d1/.npmignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "test:unit", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "cwd": "${workspaceFolder}", 13 | "env": { 14 | "NODE_ENV": "test" 15 | }, 16 | "args": ["--detectOpenHandles", "--no-cache", "--runInBand", "--rootDir", "./"], 17 | "console": "integratedTerminal", 18 | "internalConsoleOptions": "neverOpen", 19 | "stopOnEntry": false, 20 | "remoteRoot": "${workspaceFolder}", 21 | "localRoot": "${workspaceFolder}", 22 | "disableOptimisticBPs": true 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "test:unit current file", 28 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 29 | "cwd": "${workspaceFolder}", 30 | "env": { 31 | "NODE_ENV": "test" 32 | }, 33 | "args": ["--detectOpenHandles", "--no-cache", "--runInBand", "--rootDir", "./", "${file}"], 34 | "console": "integratedTerminal", 35 | "internalConsoleOptions": "neverOpen", 36 | "stopOnEntry": false, 37 | "remoteRoot": "${workspaceFolder}", 38 | "localRoot": "${workspaceFolder}", 39 | "disableOptimisticBPs": true 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "jestrunner.runOptions": ["--no-cache"], 4 | "jest.enableInlineErrorMessages": true, 5 | "eslint.workingDirectories": ["src/**/*.ts", "examples/**/*.ts", "__test__/**/*.ts"], 6 | "eslint.alwaysShowStatus": true, 7 | "eslint.format.enable": true, 8 | "eslint.options": {}, 9 | "cSpell.words": ["Birdland", "keyof", "prepend", "tsdoc", "unscoped"], 10 | "files.associations": { 11 | "api-extractor.json": "jsonc" 12 | }, 13 | "auto-close-tag.activationOnLanguage": ["html", "xml", "javascript", "javascriptreact", "typescriptreact"] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "API Tests", 8 | "type": "npm", 9 | "script": "testapi", 10 | "group": "test", 11 | "presentation": { 12 | "reveal": "always", 13 | "panel": "new" 14 | }, 15 | "problemMatcher": ["$msCompile"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 26 | - Trolling, insulting/derogatory comments, and personal or political attacks 27 | - Public or private harassment 28 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 29 | - Other conduct which could reasonably be considered inappropriate in a professional setting 30 | 31 | ## Our Responsibilities 32 | 33 | Project maintainers are responsible for clarifying the standards of acceptable 34 | behavior and are expected to take appropriate and fair corrective action in 35 | response to any instances of unacceptable behavior. 36 | 37 | Project maintainers have the right and responsibility to remove, edit, or 38 | reject comments, commits, code, wiki edits, issues, and other contributions 39 | that are not aligned to this Code of Conduct, or to ban temporarily or 40 | permanently any contributor for other behaviors that they deem inappropriate, 41 | threatening, offensive, or harmful. 42 | 43 | ## Scope 44 | 45 | This Code of Conduct applies both within project spaces and in public spaces 46 | when an individual is representing the project or its community. Examples of 47 | representing a project or community include using an official project e-mail 48 | address, posting via an official social media account, or acting as an appointed 49 | representative at an online or offline event. Representation of a project may be 50 | further defined and clarified by project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 55 | reported by contacting the project team at JasonCraftsCode@outlook.com. All 56 | complaints will be reviewed and investigated and will result in a response that 57 | is deemed necessary and appropriate to the circumstances. The project team is 58 | obligated to maintain confidentiality with regard to the reporter of an incident. 59 | Further details of specific enforcement policies may be posted separately. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good 62 | faith may face temporary or permanent repercussions as determined by other 63 | members of the project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at 69 | 70 | For answers to common questions about this code of conduct, see 71 | 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Follow these guidelines if you'd like to contribute to the project! 4 | 5 | --- 6 | 7 | ## Table of contents 8 | 9 | Read through these guidelines before you get started: 10 | 11 | - [Contributing](#contributing) 12 | - [Table of contents](#table-of-contents) 13 | - [Questions & concerns](#questions--concerns) 14 | - [Issues & bugs](#issues--bugs) 15 | - [Feature requests](#feature-requests) 16 | - [Submitting pull requests](#submitting-pull-requests) 17 | - [Code style](#code-style) 18 | 19 | ## Questions & concerns 20 | 21 | If you have any questions about using or developing for this project, reach out to @{user} or send an [email][1]. 22 | 23 | ## Issues & bugs 24 | 25 | Submit an [issue][2] or [pull request][3] with a fix if you find any bugs in the project. See [below](#submitting-pull-requests) for instructions on sending in pull requests, and be sure to reference the [code style guide](#code-style) first! 26 | 27 | When submitting an issue or pull request, make sure you're as detailed as possible and fill in all answers to questions asked in the templates. For example, an issue that simply states "X/Y/Z isn't working!" will be closed. 28 | 29 | ## Feature requests 30 | 31 | Submit an [issue][2] to request a new feature. Features fall into one of two categories: 32 | 33 | 1. **Major**: Major changes should be discussed via [email][1]. I'm always open to suggestions and will get back to you as soon as I can! 34 | 2. **Minor**: A minor feature can simply be added via a [pull request][3]. 35 | 36 | ## Submitting pull requests 37 | 38 | Before you do anything, make sure you check the current list of [pull requests][4] to ensure you aren't duplicating anyone's work. Then, do the following: 39 | 40 | 1. Fork the repository and make your changes in a git branch: `git checkout -b my-branch base-branch`. 41 | 2. Read and follow the [code style guidelines](#code-style). 42 | 3. Make sure your feature or fix doesn't break the project! Test thoroughly. 43 | 4. Commit your changes, and be sure to leave a detailed commit message. 44 | 5. Push your branch to your forked repo on GitHub: `git push origin my-branch` 45 | 6. [Submit a pull request][3] and hold tight! 46 | 7. If any changes are requested by the project maintainers, make them and follow this process again until the changes are merged in. 47 | 48 | ## Code style 49 | 50 | Please follow the coding style conventions detailed below: 51 | 52 | {guidelines} 53 | 54 | [1]: mailto:{email} 55 | [2]: https://github.com/{owner}/{repo}/issues/new 56 | [3]: https://github.com/{owner}/{repo}/compare 57 | [4]: https://github.com/{owner}/{repo}/pulls 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Christensen 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /__test__/ExpressionAttributes.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionAttributes } from '../src/ExpressionAttributes'; 2 | import { Table } from '../src/Table'; 3 | 4 | function buildParams(attributes: ExpressionAttributes): Table.ExpressionAttributeParams { 5 | const params = {}; 6 | ExpressionAttributes.addParams(params, attributes); 7 | return params; 8 | } 9 | 10 | it('Validate ExpressionAttributes exports', () => { 11 | expect(typeof ExpressionAttributes).toBe('function'); 12 | }); 13 | 14 | it('Validate isValidAttributeName', () => { 15 | expect(ExpressionAttributes.isValidAttributeName('a')).toBeTruthy(); 16 | expect(ExpressionAttributes.isValidAttributeName('a0')).toBeTruthy(); 17 | expect(ExpressionAttributes.isValidAttributeName('0')).toBeFalsy(); 18 | expect(ExpressionAttributes.isValidAttributeName('-')).toBeFalsy(); 19 | }); 20 | 21 | describe('Validate ExpressionAttributes', () => { 22 | it('addPath for simple name treatNameAsPath=false', () => { 23 | const attrs = new ExpressionAttributes(); 24 | attrs.treatNameAsPath = false; 25 | expect(attrs.addPath('path')).toEqual('#n0'); 26 | expect(attrs.getPaths()).toEqual({ '#n0': 'path' }); 27 | }); 28 | it('addPath for simple name', () => { 29 | const attrs = new ExpressionAttributes(); 30 | expect(attrs.addPath('path')).toEqual('#n0'); 31 | expect(attrs.getPaths()).toEqual({ '#n0': 'path' }); 32 | }); 33 | it('addPath for two part path', () => { 34 | const attrs = new ExpressionAttributes(); 35 | expect(attrs.addPath('path.subPath')).toEqual('#n0.#n1'); 36 | expect(attrs.getPaths()).toEqual({ '#n0': 'path', '#n1': 'subPath' }); 37 | }); 38 | it('addPath for 10 part path', () => { 39 | const attrs = new ExpressionAttributes(); 40 | expect(attrs.addPath('path.l1.l2.l3.l4.l5.l6.l7.l8.l9')).toEqual('#n0.#n1.#n2.#n3.#n4.#n5.#n6.#n7.#n8.#n9'); 41 | expect(attrs.getPaths()).toEqual({ 42 | '#n0': 'path', 43 | '#n1': 'l1', 44 | '#n2': 'l2', 45 | '#n3': 'l3', 46 | '#n4': 'l4', 47 | '#n5': 'l5', 48 | '#n6': 'l6', 49 | '#n7': 'l7', 50 | '#n8': 'l8', 51 | '#n9': 'l9', 52 | }); 53 | }); 54 | it('addPath for array', () => { 55 | const attrs = new ExpressionAttributes(); 56 | expect(attrs.addPath('path[1]')).toEqual('#n0[1]'); 57 | expect(attrs.getPaths()).toEqual({ '#n0': 'path' }); 58 | }); 59 | it('addPath for 3 dimensional array', () => { 60 | const attrs = new ExpressionAttributes(); 61 | expect(attrs.addPath('path[3][2][1]')).toEqual('#n0[3][2][1]'); 62 | expect(attrs.getPaths()).toEqual({ '#n0': 'path' }); 63 | }); 64 | it('addPath for multi component', () => { 65 | const attrs = new ExpressionAttributes(); 66 | expect(attrs.addPath('path.l1[1].l2.l3[2][1].l4.l5')).toEqual('#n0.#n1[1].#n2.#n3[2][1].#n4.#n5'); 67 | expect(attrs.getPaths()).toEqual({ 68 | '#n0': 'path', 69 | '#n1': 'l1', 70 | '#n2': 'l2', 71 | '#n3': 'l3', 72 | '#n4': 'l4', 73 | '#n5': 'l5', 74 | }); 75 | }); 76 | 77 | it('addPath with isReservedName', () => { 78 | const attrs = new ExpressionAttributes(); 79 | attrs.isReservedName = (): boolean => true; 80 | expect(attrs.addPath('path')).toEqual('#path'); 81 | expect(attrs.getPaths()).toEqual({ '#path': 'path' }); 82 | }); 83 | it('addPath with isValidName', () => { 84 | const attrs = new ExpressionAttributes(); 85 | attrs.isValidName = (): boolean => true; 86 | expect(attrs.addPath('path')).toEqual('path'); 87 | expect(attrs.getPaths()).toBeUndefined(); 88 | }); 89 | 90 | it('addValue string', () => { 91 | const attrs = new ExpressionAttributes(); 92 | expect(attrs.addValue('value')).toEqual(':v0'); 93 | expect(attrs.getValues()).toEqual({ ':v0': 'value' }); 94 | }); 95 | it('addValue number', () => { 96 | const attrs = new ExpressionAttributes(); 97 | expect(attrs.addValue(3)).toEqual(':v0'); 98 | expect(attrs.getValues()).toEqual({ ':v0': 3 }); 99 | }); 100 | it('addValue boolean', () => { 101 | const attrs = new ExpressionAttributes(); 102 | expect(attrs.addValue(true)).toEqual(':v0'); 103 | expect(attrs.getValues()).toEqual({ ':v0': true }); 104 | }); 105 | it('addValue null', () => { 106 | const attrs = new ExpressionAttributes(); 107 | expect(attrs.addValue(null)).toEqual(':v0'); 108 | expect(attrs.getValues()).toEqual({ ':v0': null }); 109 | }); 110 | it('addValue Buffer', () => { 111 | const attrs = new ExpressionAttributes(); 112 | const value = Buffer.from('buffer'); 113 | expect(attrs.addValue(value)).toEqual(':v0'); 114 | expect(attrs.getValues()).toEqual({ ':v0': value }); 115 | }); 116 | it('addValue Object', () => { 117 | const attrs = new ExpressionAttributes(); 118 | expect(attrs.addValue({ name1: 'value2', name2: 'value2' })).toEqual(':v0'); 119 | expect(attrs.getValues()).toEqual({ 120 | ':v0': { name1: 'value2', name2: 'value2' }, 121 | }); 122 | }); 123 | it('addValue Array', () => { 124 | const attrs = new ExpressionAttributes(); 125 | expect(attrs.addValue(['value2', 'value2'])).toEqual(':v0'); 126 | expect(attrs.getValues()).toEqual({ 127 | ':v0': ['value2', 'value2'], 128 | }); 129 | }); 130 | it('addValue Set', () => { 131 | const attrs = new ExpressionAttributes(); 132 | const value = new Set(['a', 'b', 'c']); 133 | expect(attrs.addValue(value)).toEqual(':v0'); 134 | expect(attrs.getValues()).toEqual({ ':v0': value }); 135 | }); 136 | it('addValue Set', () => { 137 | const attrs = new ExpressionAttributes(); 138 | const value = new Set([1, 2, 3]); 139 | expect(attrs.addValue(value)).toEqual(':v0'); 140 | expect(attrs.getValues()).toEqual({ ':v0': value }); 141 | }); 142 | it('addValue Set', () => { 143 | const attrs = new ExpressionAttributes(); 144 | const buff1 = Buffer.from('buffer1'); 145 | const buff2 = Buffer.from('buffer2'); 146 | const value = new Set([buff1, buff2]); 147 | expect(attrs.addValue(value)).toEqual(':v0'); 148 | expect(attrs.getValues()).toEqual({ ':v0': value }); 149 | }); 150 | 151 | it('getPaths', () => { 152 | const attrs = new ExpressionAttributes(); 153 | attrs.addPath('path1'); 154 | attrs.addPath('path2'); 155 | expect(attrs.getPaths()).toEqual({ '#n0': 'path1', '#n1': 'path2' }); 156 | }); 157 | 158 | it('getValues', () => { 159 | const attrs = new ExpressionAttributes(); 160 | attrs.addValue('value1'); 161 | attrs.addValue(2); 162 | expect(attrs.getValues()).toEqual({ ':v0': 'value1', ':v1': 2 }); 163 | }); 164 | 165 | it('addParams with no values', () => { 166 | const attrs = new ExpressionAttributes(); 167 | expect(buildParams(attrs)).toEqual({}); 168 | }); 169 | 170 | it('addParams with just name', () => { 171 | const attrs = new ExpressionAttributes(); 172 | attrs.addPath('name1'); 173 | expect(buildParams(attrs)).toEqual({ ExpressionAttributeNames: { '#n0': 'name1' } }); 174 | }); 175 | 176 | it('addParams with just value', () => { 177 | const attrs = new ExpressionAttributes(); 178 | attrs.addValue('value1'); 179 | expect(buildParams(attrs)).toEqual({ ExpressionAttributeValues: { ':v0': 'value1' } }); 180 | }); 181 | 182 | it('addParams with both name and value', () => { 183 | const attrs = new ExpressionAttributes(); 184 | attrs.addPath('name2'); 185 | attrs.addValue('value2'); 186 | expect(buildParams(attrs)).toEqual({ 187 | ExpressionAttributeNames: { '#n0': 'name2' }, 188 | ExpressionAttributeValues: { ':v0': 'value2' }, 189 | }); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /__test__/FieldsAdvanced.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable tsdoc/syntax */ 2 | // import { Condition } from '../src/Condition'; 3 | import { Fields } from '../src/Fields'; 4 | import { Model } from '../src/Model'; 5 | import { Table } from '../src/Table'; 6 | import { Update } from '../src/Update'; 7 | // import { buildUpdate } from './testCommon'; 8 | 9 | // Other field: 10 | // TableId = Fields.split({ aliases: [table.getPartitionKey(), table.getSortKey()] }); 11 | // options: delimiter, sortKeyDefault, generate 12 | // Duplicate - Set same value on two aliases, or field name + alias. 13 | // Maybe part of FieldBase like copyTo: {alias: x, toLower: true }. Though when duplicated the dup may also have a different field logic, like convert to lower case 14 | // Though having the BaseField support this allows 15 | // Composite field - both partition and sort key should be used. 16 | // - slots could also have configs like: { street: {alias: 1}, neighborhood: 0, city: 1, state,: 2, country: 3, region: {alias:}} 17 | // - actually could just support 18 | // - sport writing value to field or even slot name. 19 | // - partition key for indexes should have a "scope prefix" to ensure different access patterns don't overlap 20 | // - 21 | 22 | // 23 | // Model array: 24 | // - group Id - 25 | // - sort key - 26 | // - sort type - 27 | // - 28 | // 29 | // Access Pattern: 30 | // - secondary index 31 | // - map of models w/ type attribute 32 | // - 33 | 34 | const model = { name: 'MyModel' } as Model; 35 | /* 36 | function getTableContext(action: Table.ItemActions): Fields.TableContext { 37 | return { 38 | action: action, 39 | conditions: [], 40 | model, 41 | options: {} as Table.BaseOptions, 42 | } as Fields.TableContext; 43 | } 44 | const modelContext = ({ model } as unknown) as Fields.ModelContext; 45 | const tableContext = getTableContext('get'); 46 | const putTableContext = getTableContext('put'); 47 | const putNewTableContext = getTableContext('put-new'); 48 | const putReplaceTableContext = getTableContext('put-replace'); 49 | const updateTableContext = getTableContext('update'); 50 | */ 51 | 52 | // Advanced fields: 53 | // x Model type - add model name on Model.Put (could also validate same or set when Model.Update) 54 | // - version - could just build version into type... and use begins_width (#) to find/filter by type, could also 55 | // use between or =, >, >= w/ format #X.YY (X = major, YY = minor, where can use a-z, A-Z for additional version numbers). Would just need 56 | // some helpers to support that. Now what Models register for what types and version is another issue since a 57 | // Model could handle any X.YY where X is the same and YY is different (major version hasn't changed). With that 58 | // could just have model type embed the version like Model_1 or Model#1, then minor version would just be number 59 | // that is appended to the end of Model_1, though the biggest issue is that we need to limit the digits up front 60 | // to support between or >, >= conditions. Ideally in most cases we wouldn't need to worry about data versioning 61 | // just attribute existence, type or value. It also would add at least 3 bytes to each item 'V' + number or 62 | // '.' + 2 digit number, though version 0/1 could just be it doesn't exist. But when would you need 63 | // to use version (when would model type or attribute existence, type or value not work)? If there is an attribute 64 | // format that 65 | // x Created on date - date added to item when Model.put is called 66 | // x Updated on date - date updated when Model.update is called 67 | // x Revision - revision set to 0 in Model.put then incremented by 1 on Model.update 68 | // - required - add Model.update condition to ensure revision is same as passed (throw error if now) and increment if same 69 | // Fork - write to two different values 70 | // - Delete - Mark when delete, don't allow get/put/delete/update after delete=true 71 | // - TTL support? 72 | // - Hidden - When hidden don't show in get, query or scan 73 | // - ReadOnly - When ReadOnly don't don't allow put/delete/update after readOnly=true 74 | // - Created/Updated by user id - Use context.user 75 | // - Owner user id - 76 | // - Expiration - Use TTL 77 | // 78 | // Basic operations: 79 | // - ID generation for Table.put: partition and sort keys. 80 | // - Set on Table.put: Model type, created on date, id, date id, created by user, 81 | // - Update on Table.update: update on date, revision, updated by user, 82 | // - Don't write if equal: Delete, ReadOnly 83 | // - Don't write if not equal: Revision, Owner id 84 | // 85 | // Issue: 86 | // - How to support ID generation? Maybe models have an ID generation method that clients can use 87 | // to generate the id. Or just leave it up to the caller, though it would be nice to have a solution though 88 | // ids are generally very domain specific. 89 | // So no need for these fields (Model.create takes care of ensuring item doesn't exists/uniqueness): 90 | // - ID - unique id generated when Model.put is called (could auto handle already exists to regenerate and auto retry) 91 | // - Date Id - sortable unique-ish id generated when Model.put is called (could auto handle already exists to regenerate and auto retry) 92 | // 93 | // Advanced properties: 94 | // - On put/update remove if default (.removeIfDefault(), kind of implies adding default if not exists) 95 | // - Add default to output if empty in table 96 | // - Hide from output (Hide from table, .hideFromOutput()) 97 | // - on put/update remove if "empty" ('', null, undefined, ' ', 0, false, falsely), (.deleteIfEmpty() or .noEmpty()) 98 | // - Map old aliases on output or if empty (like nickName would map name if not present) (.outputAliases([])) 99 | // - Readonly - once set cannot change (.readonly()) 100 | // - 101 | 102 | // FieldDate inherits from FieldBase 103 | 104 | // TODO: 105 | // Add some type safety. 106 | // Need to determine who handles toModel 107 | // Fork is mainly used to put values into a index attribute (normalized like toLower) and item attribute 108 | class FieldFork implements Fields.Field { 109 | fields: Fields.Field[] = []; 110 | 111 | /** 112 | * Initialize the Field. 113 | * @param type - Name of type. 114 | * @param alias - Table attribute name to map this model property to. 115 | */ 116 | constructor(...fields: Fields.Field[]) { 117 | if (fields) this.fields = fields; 118 | } 119 | 120 | /** @inheritDoc {@inheritDoc (Fields:namespace).Field.init} */ 121 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 122 | init(name: string, model: Model): void { 123 | this.fields.forEach((field) => field.init(name, model)); 124 | } 125 | 126 | /** @inheritDoc {@inheritDoc (Fields:namespace).Field.toModel} */ 127 | toModel( 128 | name: string, 129 | tableData: Table.AttributeValuesMap, 130 | modelData: Model.ModelData, 131 | context: Fields.ModelContext, 132 | ): void { 133 | this.fields.forEach((field) => field.toModel(name, tableData, modelData, context)); 134 | } 135 | 136 | /** @inheritDoc {@inheritDoc (Fields:namespace).Field.toTable} */ 137 | toTable( 138 | name: string, 139 | modelData: Model.ModelData, 140 | tableData: Table.AttributeValuesMap, 141 | context: Fields.TableContext, 142 | ): void { 143 | this.fields.forEach((field) => field.toTable(name, modelData, tableData, context)); 144 | } 145 | 146 | /** @inheritDoc {@inheritDoc (Fields:namespace).Field.toTableUpdate} */ 147 | toTableUpdate( 148 | name: string, 149 | modelData: Model.ModelUpdate, 150 | tableData: Update.ResolverMap, 151 | context: Fields.TableContext, 152 | ): void { 153 | this.fields.forEach((field) => field.toTableUpdate?.(name, modelData, tableData, context)); 154 | } 155 | } 156 | 157 | // FieldDate inherits from FieldBase 158 | describe('When FieldFork', () => { 159 | const field = new FieldFork(); // Fields.createdDate(); 160 | field.init('fork', model); 161 | 162 | it('expect fields returns correct type', () => { 163 | expect(Array.isArray(field.fields)).toEqual(true); 164 | }); 165 | }); 166 | 167 | // FieldRevision inherits from FieldBase 168 | -------------------------------------------------------------------------------- /__test__/KeyCondition.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionAttributes } from '../src/ExpressionAttributes'; 2 | import { KeyCondition, KeyConditionExpression } from '../src/KeyCondition'; 3 | import { Table } from '../src/Table'; 4 | 5 | it('Validate Condition exports', () => { 6 | expect(typeof KeyCondition).toEqual('function'); 7 | expect(typeof KeyConditionExpression).toEqual('function'); 8 | }); 9 | 10 | function buildParams( 11 | key: Table.PrimaryKey.KeyQueryMap, 12 | ): { KeyConditionExpression?: string } & Table.ExpressionAttributeParams { 13 | const params = {}; 14 | const attributes = new ExpressionAttributes(); 15 | KeyConditionExpression.addParams(params, attributes, key); 16 | ExpressionAttributes.addParams(params, attributes); 17 | return params; 18 | } 19 | 20 | describe('Validate KeyCondition', () => { 21 | it('addParam with empty key', () => { 22 | expect(buildParams({})).toEqual({}); 23 | }); 24 | it('addParam', () => { 25 | expect(buildParams({ P: KeyCondition.eq('keyValue') })).toEqual({ 26 | KeyConditionExpression: '#n0 = :v0', 27 | ExpressionAttributeNames: { '#n0': 'P' }, 28 | ExpressionAttributeValues: { ':v0': 'keyValue' }, 29 | }); 30 | }); 31 | 32 | it('eq', () => { 33 | expect(buildParams({ P: KeyCondition.eq('keyValue') })).toEqual({ 34 | ExpressionAttributeNames: { '#n0': 'P' }, 35 | ExpressionAttributeValues: { ':v0': 'keyValue' }, 36 | KeyConditionExpression: '#n0 = :v0', 37 | }); 38 | }); 39 | 40 | it('lt', () => { 41 | const ltValue = 4; 42 | expect(buildParams({ P: KeyCondition.lt(ltValue) })).toEqual({ 43 | ExpressionAttributeNames: { '#n0': 'P' }, 44 | ExpressionAttributeValues: { ':v0': ltValue }, 45 | KeyConditionExpression: '#n0 < :v0', 46 | }); 47 | }); 48 | 49 | it('le', () => { 50 | expect(buildParams({ P: KeyCondition.le(6) })).toEqual({ 51 | ExpressionAttributeNames: { '#n0': 'P' }, 52 | ExpressionAttributeValues: { ':v0': 6 }, 53 | KeyConditionExpression: '#n0 <= :v0', 54 | }); 55 | }); 56 | 57 | it('gt', () => { 58 | expect(buildParams({ P: KeyCondition.gt(8) })).toEqual({ 59 | ExpressionAttributeNames: { '#n0': 'P' }, 60 | ExpressionAttributeValues: { ':v0': 8 }, 61 | KeyConditionExpression: '#n0 > :v0', 62 | }); 63 | }); 64 | 65 | it('ge', () => { 66 | expect(buildParams({ P: KeyCondition.ge(10) })).toEqual({ 67 | ExpressionAttributeNames: { '#n0': 'P' }, 68 | ExpressionAttributeValues: { ':v0': 10 }, 69 | KeyConditionExpression: '#n0 >= :v0', 70 | }); 71 | }); 72 | 73 | it('between', () => { 74 | expect(buildParams({ P: KeyCondition.between('a', 'z') })).toEqual({ 75 | ExpressionAttributeNames: { '#n0': 'P' }, 76 | ExpressionAttributeValues: { ':v0': 'a', ':v1': 'z' }, 77 | KeyConditionExpression: '#n0 BETWEEN :v0 AND :v1', 78 | }); 79 | }); 80 | 81 | it('beginsWith', () => { 82 | expect(buildParams({ P: KeyCondition.beginsWith('abc') })).toEqual({ 83 | ExpressionAttributeNames: { '#n0': 'P' }, 84 | ExpressionAttributeValues: { ':v0': 'abc' }, 85 | KeyConditionExpression: 'begins_with(#n0, :v0)', 86 | }); 87 | }); 88 | 89 | it('buildInput with KeyConditionExpression', () => { 90 | expect(buildParams({ P: KeyCondition.eq('with exp') })).toEqual({ 91 | ExpressionAttributeNames: { '#n0': 'P' }, 92 | ExpressionAttributeValues: { ':v0': 'with exp' }, 93 | KeyConditionExpression: '#n0 = :v0', 94 | }); 95 | }); 96 | 97 | it('buildInput with 2 keys', () => { 98 | expect(buildParams({ P: 'abc', S: KeyCondition.beginsWith('with exp') })).toEqual({ 99 | ExpressionAttributeNames: { '#n0': 'P', '#n1': 'S' }, 100 | ExpressionAttributeValues: { ':v0': 'abc', ':v1': 'with exp' }, 101 | KeyConditionExpression: '#n0 = :v0 AND begins_with(#n1, :v1)', 102 | }); 103 | }); 104 | 105 | it('buildInput with 3 keys', () => { 106 | expect(buildParams({ P: 'abc', S: KeyCondition.beginsWith('with exp'), E: 'extraKey' })).toEqual({ 107 | ExpressionAttributeNames: { '#n0': 'P', '#n1': 'S', '#n2': 'E' }, 108 | ExpressionAttributeValues: { ':v0': 'abc', ':v1': 'with exp', ':v2': 'extraKey' }, 109 | KeyConditionExpression: '#n0 = :v0 AND begins_with(#n1, :v1)', 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /__test__/TableIndex.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import { AWSError, Request } from 'aws-sdk'; 3 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 4 | 5 | import { Table } from '../src/Table'; 6 | import { Index } from '../src/TableIndex'; 7 | import { validateTable } from '../src/TableValidate'; 8 | import { delay } from './testCommon'; 9 | 10 | const client = new DocumentClient({ convertEmptyValues: true }); 11 | const request = { 12 | promise() { 13 | return delay(1, { Attributes: {} }); 14 | }, 15 | } as Request; 16 | 17 | it('Validate Index exports', () => { 18 | expect(typeof Index.createIndex).toBe('function'); 19 | }); 20 | 21 | describe('Validate indexes', () => { 22 | interface TestTableKey { 23 | P: Table.PrimaryKey.PartitionString; 24 | S?: Table.PrimaryKey.SortString; 25 | } 26 | 27 | const testTable = Table.createTable({ 28 | name: 'TestTable', 29 | keyAttributes: { 30 | P: Table.PrimaryKey.StringType, 31 | S: Table.PrimaryKey.StringType, 32 | G0P: Table.PrimaryKey.StringType, 33 | G0S: Table.PrimaryKey.StringType, 34 | L0S: Table.PrimaryKey.NumberType, 35 | }, 36 | keySchema: { 37 | P: Table.PrimaryKey.PartitionKeyType, 38 | S: Table.PrimaryKey.SortKeyType, 39 | }, 40 | client, 41 | }); 42 | 43 | interface GSI0Key { 44 | G0P: Table.PrimaryKey.PartitionString; 45 | G0S?: Table.PrimaryKey.SortString; 46 | } 47 | 48 | interface LSI0Key { 49 | P: Table.PrimaryKey.PartitionString; 50 | L0S?: Table.PrimaryKey.SortNumber; 51 | } 52 | 53 | interface TestTableAttributes extends TestTableKey, GSI0Key, LSI0Key {} 54 | 55 | const gsi0 = Index.createIndex({ 56 | name: 'GSI0', 57 | keySchema: { 58 | G0P: Table.PrimaryKey.PartitionKeyType, 59 | G0S: Table.PrimaryKey.SortKeyType, 60 | }, 61 | projection: { 62 | type: 'ALL', 63 | }, 64 | table: testTable as Table, 65 | type: 'GLOBAL', 66 | }); 67 | /* 68 | const lsi0 = Index.createIndex({ 69 | name: 'LSI0', 70 | keySchema: { 71 | P: Table.PrimaryKey.PartitionKeyType, 72 | L0S: Table.PrimaryKey.SortKeyType, 73 | }, 74 | projection: { 75 | attributes: ['project', 'some', 'attributes'], 76 | type: 'INCLUDE', 77 | }, 78 | table: testTable as Table, 79 | type: 'GLOBAL', 80 | }); 81 | */ 82 | beforeEach(() => { 83 | jest.clearAllMocks(); 84 | }); 85 | 86 | it('Table with Index', () => { 87 | expect(() => validateTable(testTable)).not.toThrow(); 88 | }); 89 | 90 | it('Index.getPartitionKey', () => { 91 | expect(gsi0.getPartitionKey()).toEqual('G0P'); 92 | }); 93 | 94 | it('Index.getSortKey', () => { 95 | expect(gsi0.getSortKey()).toEqual('G0S'); 96 | }); 97 | 98 | it('gsi queryParams with P', () => { 99 | const params = gsi0.queryParams({ G0P: 'mno' }); 100 | expect(params).toEqual({ 101 | ExpressionAttributeNames: { '#n0': 'G0P' }, 102 | ExpressionAttributeValues: { ':v0': 'mno' }, 103 | IndexName: 'GSI0', 104 | KeyConditionExpression: '#n0 = :v0', 105 | TableName: 'TestTable', 106 | }); 107 | }); 108 | 109 | it('gsi queryParams with G0P and G0S', () => { 110 | const params = gsi0.queryParams({ G0P: 'mno', G0S: '123' }); 111 | expect(params).toEqual({ 112 | ExpressionAttributeNames: { '#n0': 'G0P', '#n1': 'G0S' }, 113 | ExpressionAttributeValues: { ':v0': 'mno', ':v1': '123' }, 114 | IndexName: 'GSI0', 115 | KeyConditionExpression: '#n0 = :v0 AND #n1 = :v1', 116 | TableName: 'TestTable', 117 | }); 118 | }); 119 | 120 | it('gsi query', async () => { 121 | client.query = jest.fn(() => request); 122 | const results = await gsi0.query({ G0P: 'zyx', G0S: '321' }); 123 | expect(results).toEqual({ Attributes: {} }); 124 | expect(client.query).toBeCalledWith({ 125 | ExpressionAttributeNames: { '#n0': 'G0P', '#n1': 'G0S' }, 126 | ExpressionAttributeValues: { ':v0': 'zyx', ':v1': '321' }, 127 | IndexName: 'GSI0', 128 | KeyConditionExpression: '#n0 = :v0 AND #n1 = :v1', 129 | TableName: 'TestTable', 130 | }); 131 | expect(client.query).toBeCalledTimes(1); 132 | }); 133 | 134 | it('gsi scanParams', () => { 135 | const params = gsi0.scanParams(); 136 | expect(params).toEqual({ 137 | IndexName: 'GSI0', 138 | TableName: 'TestTable', 139 | }); 140 | }); 141 | 142 | it('gsi scan', async () => { 143 | client.scan = jest.fn(() => request); 144 | const results = await gsi0.scan(); 145 | expect(results).toEqual({ Attributes: {} }); 146 | expect(client.scan).toBeCalledWith({ 147 | IndexName: 'GSI0', 148 | TableName: 'TestTable', 149 | }); 150 | expect(client.scan).toBeCalledTimes(1); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Condition, 3 | ConditionExpression, 4 | ExpressionAttributes, 5 | Fields, 6 | KeyCondition, 7 | KeyConditionExpression, 8 | Model, 9 | Index, 10 | Table, 11 | validateTable, 12 | validateIndex, 13 | validateIndexes, 14 | Update, 15 | UpdateExpression, 16 | } from '../src/index'; 17 | 18 | it('Validate top level exports', () => { 19 | expect(typeof Condition).toBe('function'); 20 | expect(typeof ConditionExpression).toBe('function'); 21 | expect(typeof ExpressionAttributes).toBe('function'); 22 | expect(typeof Fields).toBe('function'); 23 | expect(typeof KeyCondition).toBe('function'); 24 | expect(typeof KeyConditionExpression).toBe('function'); 25 | expect(typeof Model).toBe('function'); 26 | expect(typeof Index).toBe('function'); 27 | expect(typeof Table).toBe('function'); 28 | expect(typeof validateTable).toBe('function'); 29 | expect(typeof validateIndex).toBe('function'); 30 | expect(typeof validateIndexes).toBe('function'); 31 | expect(typeof Update).toBe('function'); 32 | expect(typeof UpdateExpression).toBe('function'); 33 | }); 34 | -------------------------------------------------------------------------------- /__test__/testCommon.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionAttributes } from '../src/ExpressionAttributes'; 2 | import { Table } from '../src/Table'; 3 | import { Update, UpdateExpression } from '../src/Update'; 4 | 5 | // t = milliseconds 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 7 | export function delay(tms: number, v: any): Promise { 8 | return new Promise((resolve) => { 9 | setTimeout(resolve.bind(null, v), tms); 10 | }); 11 | } 12 | /* 13 | export function delayCallback(tms: number, f: () => void) { 14 | return new Promise((resolve, reject) => { 15 | f(); 16 | }); 17 | } 18 | */ 19 | 20 | export function buildUpdateParams( 21 | updateMap?: Update.ResolverMap, 22 | ): { UpdateExpression?: string } & Table.ExpressionAttributeParams { 23 | const params = {}; 24 | const attributes = new ExpressionAttributes(); 25 | UpdateExpression.addParams(params, attributes, updateMap); 26 | ExpressionAttributes.addParams(params, attributes); 27 | return params; 28 | } 29 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #000000; 3 | --dark-hl-0: #D4D4D4; 4 | --light-hl-1: #AF00DB; 5 | --dark-hl-1: #C586C0; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0000FF; 13 | --dark-hl-5: #569CD6; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #795E26; 17 | --dark-hl-7: #DCDCAA; 18 | --light-hl-8: #267F99; 19 | --dark-hl-8: #4EC9B0; 20 | --light-hl-9: #098658; 21 | --dark-hl-9: #B5CEA8; 22 | --light-code-background: #F5F5F5; 23 | --dark-code-background: #1E1E1E; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { :root { 27 | --hl-0: var(--light-hl-0); 28 | --hl-1: var(--light-hl-1); 29 | --hl-2: var(--light-hl-2); 30 | --hl-3: var(--light-hl-3); 31 | --hl-4: var(--light-hl-4); 32 | --hl-5: var(--light-hl-5); 33 | --hl-6: var(--light-hl-6); 34 | --hl-7: var(--light-hl-7); 35 | --hl-8: var(--light-hl-8); 36 | --hl-9: var(--light-hl-9); 37 | --code-background: var(--light-code-background); 38 | } } 39 | 40 | @media (prefers-color-scheme: dark) { :root { 41 | --hl-0: var(--dark-hl-0); 42 | --hl-1: var(--dark-hl-1); 43 | --hl-2: var(--dark-hl-2); 44 | --hl-3: var(--dark-hl-3); 45 | --hl-4: var(--dark-hl-4); 46 | --hl-5: var(--dark-hl-5); 47 | --hl-6: var(--dark-hl-6); 48 | --hl-7: var(--dark-hl-7); 49 | --hl-8: var(--dark-hl-8); 50 | --hl-9: var(--dark-hl-9); 51 | --code-background: var(--dark-code-background); 52 | } } 53 | 54 | body.light { 55 | --hl-0: var(--light-hl-0); 56 | --hl-1: var(--light-hl-1); 57 | --hl-2: var(--light-hl-2); 58 | --hl-3: var(--light-hl-3); 59 | --hl-4: var(--light-hl-4); 60 | --hl-5: var(--light-hl-5); 61 | --hl-6: var(--light-hl-6); 62 | --hl-7: var(--light-hl-7); 63 | --hl-8: var(--light-hl-8); 64 | --hl-9: var(--light-hl-9); 65 | --code-background: var(--light-code-background); 66 | } 67 | 68 | body.dark { 69 | --hl-0: var(--dark-hl-0); 70 | --hl-1: var(--dark-hl-1); 71 | --hl-2: var(--dark-hl-2); 72 | --hl-3: var(--dark-hl-3); 73 | --hl-4: var(--dark-hl-4); 74 | --hl-5: var(--dark-hl-5); 75 | --hl-6: var(--dark-hl-6); 76 | --hl-7: var(--dark-hl-7); 77 | --hl-8: var(--dark-hl-8); 78 | --hl-9: var(--dark-hl-9); 79 | --code-background: var(--dark-code-background); 80 | } 81 | 82 | .hl-0 { color: var(--hl-0); } 83 | .hl-1 { color: var(--hl-1); } 84 | .hl-2 { color: var(--hl-2); } 85 | .hl-3 { color: var(--hl-3); } 86 | .hl-4 { color: var(--hl-4); } 87 | .hl-5 { color: var(--hl-5); } 88 | .hl-6 { color: var(--hl-6); } 89 | .hl-7 { color: var(--hl-7); } 90 | .hl-8 { color: var(--hl-8); } 91 | .hl-9 { color: var(--hl-9); } 92 | pre, code { background: var(--code-background); } 93 | -------------------------------------------------------------------------------- /docs/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonCraftsCode/dynamodb-datamodel/2b3900d909db5838687af6cb636fadec052ec5d1/docs/assets/icons.png -------------------------------------------------------------------------------- /docs/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonCraftsCode/dynamodb-datamodel/2b3900d909db5838687af6cb636fadec052ec5d1/docs/assets/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonCraftsCode/dynamodb-datamodel/2b3900d909db5838687af6cb636fadec052ec5d1/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonCraftsCode/dynamodb-datamodel/2b3900d909db5838687af6cb636fadec052ec5d1/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/interfaces/fields.attributedefinition.html: -------------------------------------------------------------------------------- 1 | AttributeDefinition | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Defines the table attributes used by a field.

3 |

Hierarchy

  • AttributeDefinition

Index

Properties

Properties

4 |

The type of the table attribute the field writes.

5 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/fields.createddateoptions.html: -------------------------------------------------------------------------------- 1 | CreatedDateOptions | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Options for {@link CreatedDate} class constructor.

3 |

Hierarchy

  • CreatedDateOptions

Index

Properties

Methods

Properties

alias?: string
4 |

Table attribute to map this Model property to.

5 |

Methods

  • now(): Date
  • 6 |

    Function to get the current date.

    7 |

    Returns Date

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/fields.splitoptions.html: -------------------------------------------------------------------------------- 1 | SplitOptions | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Options to construct FieldSplit with.

3 |

Hierarchy

  • SplitOptions

Index

Properties

aliases: string[]
4 |

Array of table attribute names to map this model property to.

5 |
delimiter?: string
6 |

Delimiter to use for splitting the model property string.

7 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/fields.typeoptions.html: -------------------------------------------------------------------------------- 1 | TypeOptions | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

FieldType constructor options.

3 |

Hierarchy

  • TypeOptions

Index

Properties

Properties

alias?: string
4 |

Table attribute to map this Model property to.

5 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/index.defaultglobalindexkey.html: -------------------------------------------------------------------------------- 1 | DefaultGlobalIndexKey | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Default and Example global secondary index primary key with the generalized compact format of.

3 |

Hierarchy

  • DefaultGlobalIndexKey

Index

Properties

Properties

4 |

Partition key: G#P which represents G = Global + # = index number + P = Partition key.

5 |
6 |

Sort key: G#S which represents G = Global + # = index number + S = Sort key. The sort key is optional 7 | to support the sort key as being option for queryParams and query methods.

8 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/index.defaultlocalindexkey.html: -------------------------------------------------------------------------------- 1 | DefaultLocalIndexKey | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Default and Example local secondary index primary key with the generalized compact format of.

3 |

Hierarchy

  • DefaultLocalIndexKey

Index

Properties

Properties

4 |

Sort key: L#S which represents L = Local + # = index number + S = Sort key. The sort key is optional 5 | to support the sort key as being option for queryParams and query methods.

6 |
7 |

Partition key: P which is the Table partition key since local secondary indexes are stored in the 8 | same partition as the main table.

9 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/model.baseoutput.html: -------------------------------------------------------------------------------- 1 | BaseOutput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface BaseOutput<ITEM, RESULT>

2 |

Base output type for the Model read/write methods.

3 |

Type parameters

  • ITEM

  • RESULT

Hierarchy

Index

Properties

Properties

item: ModelOutT<ITEM>
4 |

Model item data read from the table.

5 |
result: RESULT
6 |

The result of the table read/write.

7 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.batchgettableinput.html: -------------------------------------------------------------------------------- 1 | BatchGetTableInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input per table params for DocumentClient.batchGet 3 | Removes legacy parameters from the type definition, including AttributesToGet

4 |

Hierarchy

  • Omit<DocumentClient.KeysAndAttributes, "AttributesToGet">
    • BatchGetTableInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.defaulttablekey.html: -------------------------------------------------------------------------------- 1 | DefaultTableKey | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Default and Example table primary key with a generalized compact format.

3 |

Hierarchy

  • DefaultTableKey

Index

Properties

Properties

4 |

Table partition key.

5 |
6 |

Table sort key. The sort key is optional to support the sort key as being option for queryParams 7 | and query methods.

8 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.deleteinput.html: -------------------------------------------------------------------------------- 1 | DeleteInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.delete 3 | Removes legacy parameters from the type definition, including Expected and ConditionalOperator

4 |

Hierarchy

  • Omit<DocumentClient.DeleteItemInput, "Expected" | "ConditionalOperator">
    • DeleteInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.getinput.html: -------------------------------------------------------------------------------- 1 | GetInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.get 3 | Removes legacy parameters from the type definition, including AttributesToGet.

4 |

Hierarchy

  • Omit<DocumentClient.GetItemInput, "AttributesToGet">
    • GetInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.primarykey.keyquery.html: -------------------------------------------------------------------------------- 1 | KeyQuery | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Attribute query value of the primary key for tables and indexes.

3 |

Hierarchy

  • KeyQuery

Index

Properties

Properties

4 |

Partition key query value.

5 |
6 |

Sort key query value.

7 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.putinput.html: -------------------------------------------------------------------------------- 1 | PutInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.put 3 | Removes legacy parameters from the type definition, including Expected and ConditionalOperator

4 |

Hierarchy

  • Omit<DocumentClient.PutItemInput, "Expected" | "ConditionalOperator">
    • PutInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.putitem.html: -------------------------------------------------------------------------------- 1 | PutItem | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Key and item attributes for used when putting and item

3 |

Hierarchy

Index

Properties

Properties

4 |

Attributes of the item to put.

5 |
6 |

Primary key of item to put.

7 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.queryinput.html: -------------------------------------------------------------------------------- 1 | QueryInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.query 3 | Removes legacy parameters from the type definition, including AttributesToGet, KeyConditions, QueryFilter and ConditionalOperator

4 |

Hierarchy

  • Omit<DocumentClient.QueryInput, "AttributesToGet" | "KeyConditions" | "QueryFilter" | "ConditionalOperator">
    • QueryInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.scaninput.html: -------------------------------------------------------------------------------- 1 | ScanInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.scan 3 | Removes legacy parameters from the type definition, including AttributesToGet, QueryFilter and ConditionalOperator

4 |

Hierarchy

  • Omit<DocumentClient.ScanInput, "AttributesToGet" | "ScanFilter" | "ConditionalOperator">
    • ScanInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.transactgetitem.html: -------------------------------------------------------------------------------- 1 | TransactGetItem | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Param to transactGet* methods

3 |

Hierarchy

Index

Properties

itemAttributes?: string[]
4 |

Attributes of the items to get.

5 |
6 |

Primary key of items to get.

7 |

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/table.updateinput.html: -------------------------------------------------------------------------------- 1 | UpdateInput | dynamodb-datamodel - v0.2.7
Options
All
  • Public
  • Public/Protected
  • All
Menu
2 |

Input params for DocumentClient.update 3 | Removes legacy parameters from the type definition, including AttributeUpdates, Expected and ConditionalOperator

4 |

Hierarchy

  • Omit<DocumentClient.UpdateItemInput, "AttributeUpdates" | "Expected" | "ConditionalOperator">
    • UpdateInput

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /etc/readme.md: -------------------------------------------------------------------------------- 1 | # API extractor 2 | 3 | This folder contains the md file api-extractor extracts from typescript. The intent of this is to make it easy to understand the API changes and API documentation for dynamodb-datamodel. 4 | -------------------------------------------------------------------------------- /examples/Condition.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Table } from 'dynamodb-datamodel'; 2 | 3 | // Destructuring methods from Condition to make writing expression more concise 4 | const { eq, ne, and, path } = Condition; 5 | const condition = and(eq('first', 'john'), eq('last', 'smith'), ne('first', path('nickname'))); 6 | 7 | // Table params generate for condition 8 | expect(Table.addParams({}, { conditions: [condition] })).toEqual({ 9 | ConditionExpression: '(#n0 = :v0 AND #n1 = :v1 AND #n0 <> #n2)', 10 | ExpressionAttributeNames: { '#n0': 'first', '#n1': 'last', '#n2': 'nickname' }, 11 | ExpressionAttributeValues: { ':v0': 'john', ':v1': 'smith' }, 12 | }); 13 | -------------------------------------------------------------------------------- /examples/Fields.composite.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table, gsi0 } from './Table'; 3 | 4 | // (TypeScript) Define model key and item interface. 5 | interface ModelKey { 6 | id: string; 7 | } 8 | // street, city, state and country only support simple set updates since they are part of a composite key 9 | interface ModelItem extends ModelKey { 10 | neighborhood: string; 11 | city: string; 12 | state: string; 13 | country: string; 14 | region: Update.String; 15 | } 16 | 17 | // Create composite slots to use in model schema below. 18 | const location = Fields.composite({ alias: gsi0.getSortKey(), count: 4, delimiter: ';' }); 19 | const locSlots = location.createSlots(); 20 | 21 | // Define the schema using Fields 22 | const model = Model.createModel({ 23 | schema: { 24 | id: Fields.split({ aliases: ['P', 'S'] }), 25 | neighborhood: locSlots[3], 26 | city: locSlots[2], 27 | state: locSlots[1], 28 | country: locSlots[0], 29 | region: Fields.string({ alias: gsi0.getPartitionKey() }), 30 | }, 31 | table: table as Table, 32 | }); 33 | 34 | const params = model.putParams({ 35 | id: 'p1.s2', 36 | neighborhood: 'Birdland', 37 | city: 'Cupertino', 38 | state: 'CA', 39 | country: 'USA', 40 | region: 'NorthAmerica', 41 | }); 42 | 43 | // test: value to expect 44 | expect(params).toEqual({ 45 | Item: { G0P: 'NorthAmerica', G0S: 'USA;CA;Cupertino;Birdland', P: 'p1', S: 's2' }, 46 | TableName: 'ExampleTable', 47 | }); 48 | -------------------------------------------------------------------------------- /examples/Fields.compositeNamed.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table, gsi0 } from './Table'; 3 | 4 | // Create composite slots to use in model schema below. 5 | const locMap = { neighborhood: 3, city: 2, state: 1, country: 0 }; 6 | const location = Fields.compositeNamed({ 7 | alias: gsi0.getSortKey(), 8 | map: locMap, 9 | delimiter: ';', 10 | }); 11 | 12 | interface ModelIdKey { 13 | id: string; 14 | } 15 | 16 | // neighborhood, city, state and country only support simple set updates since they are part of a composite key 17 | interface ModelItem extends ModelIdKey { 18 | neighborhood: string; 19 | city: string; 20 | state: string; 21 | country: string; 22 | region: Update.String; 23 | } 24 | 25 | const locSlots = location.createNamedSlots(); 26 | 27 | // Define the schema using Fields 28 | const model = Model.createModel({ 29 | schema: { 30 | id: Fields.split({ aliases: [table.getPartitionKey(), table.getSortKey()] }), 31 | neighborhood: locSlots.neighborhood, 32 | city: locSlots.city, 33 | state: locSlots.state, 34 | country: locSlots.country, 35 | region: Fields.string({ alias: gsi0.getPartitionKey() }), 36 | }, 37 | table: table as Table, 38 | }); 39 | 40 | const params = model.putParams({ 41 | id: 'P-1.S-2', 42 | neighborhood: 'Birdland', 43 | city: 'Cupertino', 44 | state: 'CA', 45 | country: 'USA', 46 | region: 'NorthAmerica', 47 | }); 48 | 49 | // test: value to expect 50 | expect(params).toEqual({ 51 | Item: { G0P: 'NorthAmerica', G0S: 'USA;CA;Cupertino;Birdland', P: 'P-1', S: 'S-2' }, 52 | TableName: 'ExampleTable', 53 | }); 54 | -------------------------------------------------------------------------------- /examples/Fields.split.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // (TypeScript) Define model key and item interface. 5 | interface ModelKey { 6 | id: string; 7 | } 8 | 9 | // Define the schema using Fields 10 | const model = Model.createModel({ 11 | schema: { 12 | id: Fields.split({ aliases: ['P', 'S'] }), 13 | }, 14 | table: table as Table, 15 | }); 16 | 17 | // Generate params to pass to DocumentClient or call the action method 18 | const params = model.getParams({ id: 'P-1.S-1' }); 19 | 20 | // (jest) output of getParams 21 | expect(params).toEqual({ Key: { P: 'P-1', S: 'S-1' }, TableName: 'ExampleTable' }); 22 | -------------------------------------------------------------------------------- /examples/Fields.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // (TypeScript) Define model key and item interface. 5 | export interface ModelKey { 6 | id: string; 7 | } 8 | // Use Update types so model.update will have some type safety. 9 | export interface ModelItem extends ModelKey { 10 | name: Update.String; 11 | age?: Update.Number; 12 | children?: Update.List<{ name: string; age: number }>; 13 | sports?: Update.StringSet; 14 | } 15 | 16 | // Define the schema using Fields 17 | export const model = Model.createModel({ 18 | schema: { 19 | id: Fields.split({ aliases: ['P', 'S'] }), 20 | name: Fields.string(), 21 | age: Fields.number(), 22 | children: Fields.list(), 23 | sports: Fields.stringSet(), 24 | }, 25 | table: table as Table, 26 | }); 27 | 28 | // Generate params to pass to DocumentClient or call the action method 29 | const params = model.updateParams({ id: 'P-1.S-1', age: Update.inc(1) }); 30 | 31 | // (jest) output of updateParams 32 | expect(params).toEqual({ 33 | ExpressionAttributeNames: { '#n0': 'age' }, 34 | ExpressionAttributeValues: { ':v0': 1 }, 35 | Key: { P: 'P-1', S: 'S-1' }, 36 | TableName: 'ExampleTable', 37 | ReturnValues: 'ALL_NEW', 38 | UpdateExpression: 'SET #n0 = #n0 + :v0', 39 | }); 40 | -------------------------------------------------------------------------------- /examples/Fields.type.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // (TypeScript) Define model key and item interface. 5 | interface ModelKey { 6 | id: string; 7 | } 8 | interface ModelItem extends ModelKey { 9 | typename?: Update.String; 10 | } 11 | 12 | // Define the schema using Fields 13 | const model = Model.createModel({ 14 | name: 'ExampleModel', 15 | schema: { 16 | id: Fields.split({ aliases: ['P', 'S'] }), 17 | typename: Fields.type({ alias: 'T' }), 18 | }, 19 | table: table as Table, 20 | }); 21 | 22 | // Generate params to pass to DocumentClient or call the action method 23 | const params = model.putParams({ id: 'P-1.S-1' }); 24 | 25 | // (jest) output of putParams 26 | expect(params).toEqual({ Item: { P: 'P-1', S: 'S-1', T: 'ExampleModel' }, TableName: 'ExampleTable' }); 27 | -------------------------------------------------------------------------------- /examples/Index.ts: -------------------------------------------------------------------------------- 1 | import { Index, Table } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // Define a Global Secondary Index (GSI) key interface for GSI0. 5 | export interface GSI0Key { 6 | G0P: Table.PrimaryKey.PartitionString; 7 | G0S?: Table.PrimaryKey.SortString; 8 | } 9 | 10 | // Create an Index object for GSI0 based on GSI0Key, and project all attributes. 11 | export const gsi0 = Index.createIndex({ 12 | name: 'GSI0', 13 | // Defines the key type ('HASH' or 'RANGE') for the GSI primary keys. 14 | keySchema: { 15 | G0P: Table.PrimaryKey.PartitionKeyType, 16 | G0S: Table.PrimaryKey.SortKeyType, 17 | }, 18 | projection: { type: 'ALL' }, 19 | table: table as Table, 20 | type: 'GLOBAL', 21 | }); 22 | 23 | // Define a Local Secondary Index (LSI) key interface for LSI0, partition key must be same as the table's 24 | export interface LSI0Key { 25 | P: Table.PrimaryKey.PartitionString; 26 | L0S?: Table.PrimaryKey.SortNumber; 27 | } 28 | 29 | // Create an Index object for LSI0 based on LSI0Key, and project all attributes. 30 | export const lsi0 = Index.createIndex({ 31 | name: 'LSI0', 32 | // Defines the key type ('HASH' or 'RANGE') for the LSI primary keys. 33 | keySchema: { 34 | P: Table.PrimaryKey.PartitionKeyType, 35 | L0S: Table.PrimaryKey.SortKeyType, 36 | }, 37 | projection: { type: 'ALL' }, 38 | table: table as Table, 39 | type: 'LOCAL', 40 | }); 41 | -------------------------------------------------------------------------------- /examples/KeyCondition.ts: -------------------------------------------------------------------------------- 1 | import { KeyCondition } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // Use KeyCondition to query the table with primary key of 'P-GUID' and sort key between (and including) 'a' and 'z' 5 | const key = { 6 | P: 'P-GUID', 7 | S: KeyCondition.between('a', 'z'), 8 | }; 9 | const params = table.queryParams(key); 10 | 11 | expect(params).toEqual({ 12 | ExpressionAttributeNames: { '#n0': 'P', '#n1': 'S' }, 13 | ExpressionAttributeValues: { ':v0': 'P-GUID', ':v1': 'a', ':v2': 'z' }, 14 | KeyConditionExpression: '#n0 = :v0 AND #n1 BETWEEN :v1 AND :v2', 15 | TableName: 'ExampleTable', 16 | }); 17 | -------------------------------------------------------------------------------- /examples/Model.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // (TypeScript) Define model key and item interface. 5 | export interface ModelKey { 6 | id: string; 7 | } 8 | // Use Update.* types to support type checking when using Model.update. 9 | export interface ModelItem extends ModelKey { 10 | name: Update.String; 11 | age?: Update.Number; 12 | children?: Update.List<{ name: string; age: number }>; 13 | sports?: Update.StringSet; 14 | } 15 | 16 | // Define the schema using Fields 17 | export const model = Model.createModel({ 18 | schema: { 19 | id: Fields.split({ aliases: ['P', 'S'] }), 20 | name: Fields.string(), 21 | age: Fields.number(), 22 | children: Fields.list(), 23 | sports: Fields.stringSet(), 24 | }, 25 | table: table as Table, 26 | }); 27 | 28 | // Generate params to pass to DocumentClient or call the action method 29 | const params = model.getParams({ id: 'P-1.S-1' }); 30 | 31 | // (jest) output of getParams 32 | expect(params).toEqual({ Key: { P: 'P-1', S: 'S-1' }, TableName: 'ExampleTable' }); 33 | -------------------------------------------------------------------------------- /examples/Readme.BasicUsage.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 2 | // 1. Import or require `Table`, `Model` and `Fields` from `dynamodb-datamodel` 3 | import { Table, Model, Fields } from 'dynamodb-datamodel'; 4 | 5 | // 2. Create DynamoDB DocumentClient 6 | const client = new DocumentClient({ convertEmptyValues: true }); 7 | 8 | // 3. (TypeScript) Define Table's primary key 9 | interface TableKey { 10 | P: Table.PrimaryKey.PartitionString; 11 | S?: Table.PrimaryKey.SortString; 12 | } 13 | 14 | // 4. Create Table and define key attributes and schema 15 | const table = Table.createTable({ 16 | client, 17 | name: 'SimpleTable', 18 | keyAttributes: { 19 | P: Table.PrimaryKey.StringType, 20 | S: Table.PrimaryKey.StringType, 21 | }, 22 | keySchema: { 23 | P: Table.PrimaryKey.PartitionKeyType, 24 | S: Table.PrimaryKey.SortKeyType, 25 | }, 26 | }); 27 | 28 | // 5. (TypeScript) Define each Model key and data interface 29 | interface ModelKey { 30 | id: string; 31 | } 32 | // Define model data that derives from the key 33 | interface ModelItem extends ModelKey { 34 | name: string; 35 | } 36 | 37 | // 6. Create each Model and define data schema 38 | const model = Model.createModel({ 39 | schema: { 40 | id: Fields.split({ aliases: ['P', 'S'] }), 41 | name: Fields.string(), 42 | }, 43 | table: table as Table, // 'as Table' needed for TypeScript 44 | }); 45 | 46 | // Additional models can also be defined 47 | 48 | // 7. Use the model to read and write data 49 | export async function main(): Promise { 50 | // Write item 51 | await model.put({ id: 'P-GUID.S-0', name: 'user name' }); 52 | // Update item 53 | await model.update({ id: 'P-GUID.S-0', name: 'new user name' }); 54 | // Get item 55 | await model.get({ id: 'P-GUID.S-0' }); 56 | // Delete item 57 | await model.delete({ id: 'P-GUID.S-0' }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/Readme.Condition.Fields.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Fields, Model, Table } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | const schema = { 5 | age: Fields.number(), 6 | region: Fields.string(), 7 | interests: Fields.string(), 8 | }; 9 | 10 | // Assigning the schema to a model will initialize the schema fields to use below. 11 | new Model({ name: 'TestModel', schema, table: table as Table }); 12 | 13 | // Destructuring schema and Condition to make it easier to write filter expression. 14 | const { age, region, interests } = schema; 15 | const { and, or, gt } = Condition; 16 | 17 | const filter = or( 18 | age.gt(21), 19 | and( 20 | region.eq('US'), 21 | gt(interests.size(), 10), 22 | or(interests.contains('nodejs'), interests.contains('dynamodb'), interests.contains('serverless')), 23 | ), 24 | ); 25 | 26 | // build and validate expression 27 | const params = Table.addParams({}, { filters: [filter] }); 28 | expect(params.FilterExpression).toEqual( 29 | '(#n0 > :v0 OR (#n1 = :v1 AND size(#n2) > :v2 AND (contains(#n2, :v3) OR contains(#n2, :v4) OR contains(#n2, :v5))))', 30 | ); 31 | -------------------------------------------------------------------------------- /examples/Readme.Condition.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Table } from 'dynamodb-datamodel'; 2 | 3 | // Destructuring Condition to make it easier to write filter expression. 4 | const { and, or, eq, gt, contains, size } = Condition; 5 | 6 | const filter = or( 7 | gt('age', 21), 8 | and( 9 | eq('region', 'US'), 10 | gt(size('interests'), 10), 11 | or(contains('interests', 'nodejs'), contains('interests', 'dynamodb'), contains('interests', 'serverless')), 12 | ), 13 | ); 14 | 15 | const params = Table.addParams({}, { filters: [filter] }); 16 | expect(params.FilterExpression).toEqual( 17 | '(#n0 > :v0 OR (#n1 = :v1 AND size(#n2) > :v2 AND (contains(#n2, :v3) OR contains(#n2, :v4) OR contains(#n2, :v5))))', 18 | ); 19 | -------------------------------------------------------------------------------- /examples/Table.Simple.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 2 | import { Table } from 'dynamodb-datamodel'; 3 | 4 | export const client = new DocumentClient({ convertEmptyValues: true }); 5 | 6 | // Define the table primary key interface. 7 | export interface TableKey { 8 | P: Table.PrimaryKey.PartitionString; 9 | S?: Table.PrimaryKey.SortString; 10 | } 11 | 12 | // Create the table object for the primary key and secondary indexes. 13 | export const table = Table.createTable({ 14 | client, 15 | name: 'SimpleTable', 16 | keyAttributes: { 17 | P: Table.PrimaryKey.StringType, 18 | S: Table.PrimaryKey.StringType, 19 | }, 20 | keySchema: { 21 | P: Table.PrimaryKey.PartitionKeyType, 22 | S: Table.PrimaryKey.SortKeyType, 23 | }, 24 | }); 25 | 26 | // Generate params to pass to DocumentClient or call the action method 27 | const params = table.getParams({ P: 'p1', S: 's1' }); 28 | 29 | // (jest) output of getParams 30 | expect(params).toEqual({ Key: { P: 'p1', S: 's1' }, TableName: 'SimpleTable' }); 31 | -------------------------------------------------------------------------------- /examples/Table.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 2 | import { Table } from 'dynamodb-datamodel'; 3 | import { GSI0Key, gsi0, LSI0Key, lsi0 } from './Index'; 4 | export { GSI0Key, gsi0, LSI0Key, lsi0 }; // export from here to consolidate imports 5 | 6 | // Good practice to covert empty values to null 7 | export const client = new DocumentClient({ convertEmptyValues: true }); 8 | 9 | // Define the table primary key interface. 10 | export interface TableKey { 11 | P: Table.PrimaryKey.PartitionString; 12 | S?: Table.PrimaryKey.SortString; 13 | } 14 | 15 | // Define the combined primary keys across the table and secondary indexes. 16 | interface KeyAttributes extends TableKey, GSI0Key, LSI0Key {} 17 | 18 | // Create the table object with global and local secondary indexes. 19 | // name, keyAttributes and keySchema should match the DynamoDB's table CloudFormation resource. 20 | export const table = Table.createTable({ 21 | client, 22 | name: 'ExampleTable', 23 | // Defines the attribute type ('S', 'N', 'B') for all primary keys, table and indexes. 24 | keyAttributes: { 25 | P: Table.PrimaryKey.StringType, 26 | S: Table.PrimaryKey.StringType, 27 | G0P: Table.PrimaryKey.StringType, 28 | G0S: Table.PrimaryKey.StringType, 29 | L0S: Table.PrimaryKey.NumberType, 30 | }, 31 | // Defines the key type ('HASH' or 'RANGE') for the table primary keys. 32 | keySchema: { 33 | P: Table.PrimaryKey.PartitionKeyType, 34 | S: Table.PrimaryKey.SortKeyType, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /examples/Update.Model.ts: -------------------------------------------------------------------------------- 1 | import { Fields, Model, Table, Update } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | interface ModelKey { 5 | id: string; 6 | } 7 | interface ModelItem extends ModelKey { 8 | name: Update.String; 9 | revision: Update.Number; 10 | nickName: Update.String; 11 | } 12 | 13 | const model = Model.createModel({ 14 | schema: { 15 | id: Fields.split({ aliases: ['P', 'S'] }), 16 | name: Fields.string(), 17 | nickName: Fields.string(), 18 | revision: Fields.number(), 19 | }, 20 | table: table as Table, 21 | }); 22 | 23 | // update will: set name attribute to 'new name', delete nickName attribute and increment revision attribute by 2. 24 | const params = model.updateParams({ 25 | id: 'P-1.S-1', 26 | name: 'new name', 27 | nickName: Update.del(), 28 | revision: Update.inc(2), 29 | }); 30 | 31 | // (jest) output of updateParams 32 | expect(params).toEqual({ 33 | ExpressionAttributeNames: { '#n0': 'name', '#n1': 'nickName', '#n2': 'revision' }, 34 | ExpressionAttributeValues: { ':v0': 'new name', ':v1': 2 }, 35 | Key: { P: 'P-1', S: 'S-1' }, 36 | ReturnValues: 'ALL_NEW', 37 | TableName: 'ExampleTable', 38 | UpdateExpression: 'SET #n0 = :v0, #n2 = #n2 + :v1 REMOVE #n1', 39 | }); 40 | -------------------------------------------------------------------------------- /examples/Update.Table.ts: -------------------------------------------------------------------------------- 1 | import { Update } from 'dynamodb-datamodel'; 2 | import { table } from './Table'; 3 | 4 | // update will: set name attribute to 'new name', delete nickName attribute and increment revision attribute by 2. 5 | const params = table.updateParams( 6 | { P: 'P-1', S: 'S-1' }, 7 | { name: 'new name', nickName: Update.del(), revision: Update.inc(2) }, 8 | ); 9 | 10 | // (jest) output of updateParams 11 | expect(params).toEqual({ 12 | ExpressionAttributeNames: { '#n0': 'name', '#n1': 'nickName', '#n2': 'revision' }, 13 | ExpressionAttributeValues: { ':v0': 'new name', ':v1': 2 }, 14 | Key: { P: 'P-1', S: 'S-1' }, 15 | ReturnValues: 'ALL_NEW', 16 | TableName: 'ExampleTable', 17 | UpdateExpression: 'SET #n0 = :v0, #n2 = #n2 + :v1 REMOVE #n1', 18 | }); 19 | -------------------------------------------------------------------------------- /examples/baseUrl/dynamodb-datamodel.ts: -------------------------------------------------------------------------------- 1 | // Using tsconfig baseUrl to allow examples to use "import {Table} from 'dynamodb-datamodel'" 2 | export { 3 | Condition, 4 | ConditionExpression, 5 | ExpressionAttributes, 6 | Fields, 7 | KeyCondition, 8 | KeyConditionExpression, 9 | Model, 10 | Index, 11 | Table, 12 | validateTable, 13 | Update, 14 | UpdateExpression, 15 | } from '../../src/index'; 16 | -------------------------------------------------------------------------------- /examples/javascript/Fields.compositeNamed.js: -------------------------------------------------------------------------------- 1 | const { Fields, Model } = require('dynamodb-datamodel'); 2 | const { table, gsi0 } = require('./Table'); 3 | 4 | // Create composite slots to use in model schema below. 5 | const locMap = { neighborhood: 3, city: 2, state: 1, country: 0 }; 6 | const location = Fields.compositeNamed({ 7 | alias: gsi0.getSortKey(), 8 | map: locMap, 9 | delimiter: ';', 10 | }); 11 | 12 | const locSlots = location.createNamedSlots(); 13 | 14 | // Define the schema using Fields 15 | const model = new Model({ 16 | schema: { 17 | id: Fields.split({ aliases: [table.getPartitionKey(), table.getSortKey()] }), 18 | neighborhood: locSlots.neighborhood, 19 | city: locSlots.city, 20 | state: locSlots.state, 21 | country: locSlots.country, 22 | region: Fields.string({ alias: gsi0.getPartitionKey() }), 23 | }, 24 | table: table, 25 | }); 26 | 27 | const params = model.putParams({ 28 | id: 'p1.s2', 29 | neighborhood: 'Birdland', 30 | city: 'Cupertino', 31 | state: 'CA', 32 | country: 'USA', 33 | region: 'NorthAmerica', 34 | }); 35 | 36 | // test: value to expect 37 | expect(params).toEqual({ 38 | Item: { G0P: 'NorthAmerica', G0S: 'USA;CA;Cupertino;Birdland', P: 'p1', S: 's2' }, 39 | TableName: 'ExampleTable', 40 | }); 41 | -------------------------------------------------------------------------------- /examples/javascript/Index.js: -------------------------------------------------------------------------------- 1 | const { Index, Table } = require('dynamodb-datamodel'); 2 | const { table } = require('./Table'); 3 | 4 | // Create an Index object for GSI0 based on GSI0Key, and project all attributes. 5 | const gsi0 = new Index({ 6 | name: 'GSI0', 7 | // Defines the key type ('HASH' or 'RANGE') for the GSI primary keys. 8 | keySchema: { 9 | G0P: Table.PrimaryKey.PartitionKeyType, 10 | G0S: Table.PrimaryKey.SortKeyType, 11 | }, 12 | projection: { type: 'ALL' }, 13 | table, 14 | type: 'GLOBAL', 15 | }); 16 | exports.gsi0 = gsi0; 17 | 18 | // Create an Index object for LSI0 based on LSI0Key, and project all attributes. 19 | exports.lsi0 = new Index({ 20 | name: 'LSI0', 21 | // Defines the key type ('HASH' or 'RANGE') for the LSI primary keys, partition key must be same as the table's 22 | keySchema: { 23 | P: Table.PrimaryKey.PartitionKeyType, 24 | L0S: Table.PrimaryKey.SortKeyType, 25 | }, 26 | projection: { type: 'ALL' }, 27 | table, 28 | type: 'LOCAL', 29 | }); 30 | -------------------------------------------------------------------------------- /examples/javascript/Models.js: -------------------------------------------------------------------------------- 1 | const { Fields, Model } = require('dynamodb-datamodel'); 2 | const { table } = require('./Table'); 3 | 4 | // Define the schema using Fields 5 | const model = new Model({ 6 | schema: { 7 | id: Fields.split({ aliases: ['P', 'S'] }), 8 | name: Fields.string(), 9 | age: Fields.number(), 10 | children: Fields.list(), 11 | sports: Fields.stringSet(), 12 | }, 13 | table, 14 | }); 15 | 16 | const params = model.getParams({ id: 'P-1.S-1' }); 17 | 18 | expect(params).toEqual({ Key: { P: 'P-1', S: 'S-1' }, TableName: 'ExampleTable' }); 19 | -------------------------------------------------------------------------------- /examples/javascript/Table.Simple.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require('aws-sdk/clients/dynamodb'); 2 | const { Table } = require('dynamodb-datamodel'); 3 | 4 | const client = new DocumentClient({ convertEmptyValues: true }); 5 | 6 | // Create the table object for the primary key and secondary indexes. 7 | const table = new Table({ 8 | client, 9 | name: 'SimpleTable', 10 | keyAttributes: { 11 | P: Table.PrimaryKey.StringType, 12 | S: Table.PrimaryKey.StringType, 13 | }, 14 | keySchema: { 15 | P: Table.PrimaryKey.PartitionKeyType, 16 | S: Table.PrimaryKey.SortKeyType, 17 | }, 18 | }); 19 | 20 | const params = table.getParams({ P: 'p1', S: 's1' }); 21 | 22 | expect(params).toEqual({ Key: { P: 'p1', S: 's1' }, TableName: 'SimpleTable' }); 23 | -------------------------------------------------------------------------------- /examples/javascript/Table.js: -------------------------------------------------------------------------------- 1 | const { DocumentClient } = require('aws-sdk/clients/dynamodb'); 2 | const { Table } = require('dynamodb-datamodel'); 3 | const { gsi0, lsi0 } = require('./Index'); 4 | 5 | const client = new DocumentClient({ convertEmptyValues: true }); 6 | exports.client = client; 7 | 8 | // Create the table object with global and local secondary indexes. 9 | // name, keyAttributes and keySchema should match the DynamoDB's table CloudFormation resource. 10 | exports.table = new Table({ 11 | client, 12 | name: 'ExampleTable', 13 | // Defines the attribute type ('S', 'N', 'B') for all primary keys, table and indexes. 14 | keyAttributes: { 15 | P: Table.PrimaryKey.StringType, 16 | S: Table.PrimaryKey.StringType, 17 | G0P: Table.PrimaryKey.StringType, 18 | G0S: Table.PrimaryKey.StringType, 19 | L0S: Table.PrimaryKey.NumberType, 20 | }, 21 | // Defines the key type ('HASH' or 'RANGE') for the table primary keys. 22 | keySchema: { 23 | P: Table.PrimaryKey.PartitionKeyType, 24 | S: Table.PrimaryKey.SortKeyType, 25 | }, 26 | }); 27 | 28 | // export from here to consolidate imports 29 | exports.gsi0 = gsi0; 30 | exports.lsi0 = lsi0; 31 | -------------------------------------------------------------------------------- /examples/javascript/test/Fields.Examples.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/expect-expect */ 2 | it('Fields.compositeNamed', () => { 3 | require('../Fields.compositeNamed'); 4 | }); 5 | -------------------------------------------------------------------------------- /examples/javascript/test/Index.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | it('Index', () => { 3 | require('../Index'); 4 | }); 5 | -------------------------------------------------------------------------------- /examples/javascript/test/Model.examples.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/expect-expect */ 2 | it('Models', () => { 3 | require('../Models'); 4 | }); 5 | -------------------------------------------------------------------------------- /examples/javascript/test/Table.Examples.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/expect-expect */ 2 | it('Table.Simple.ts', () => { 3 | require('../Table.Simple'); 4 | }); 5 | 6 | it('Table.ts', () => { 7 | require('../Table'); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Working and Tested examples 2 | 3 | It is extremely frustrating copying example code that does not work or requires some unknown setup to work. The intend of this folder is to have examples that can be embed into documentation (typedoc supports [[include:Fields.compositeNamed.ts]] based embedding) and also make it easy for user to copy and paste fully working examples. 4 | 5 | This also allows the examples to be linted, prettied, built and tested. 6 | 7 | All of the examples will be in typescript, but some will also be in javascript focusing on the examples that use generics and key or model interfaces. 8 | -------------------------------------------------------------------------------- /examples/test/Condition.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | import { Condition, Table } from 'dynamodb-datamodel'; 3 | 4 | it('Condition', () => { 5 | require('../Condition'); 6 | }); 7 | 8 | // Validate each example for Condition functions 9 | it('Condition.path', () => { 10 | const condition = Condition.eq('name', Condition.path('nickname')); 11 | 12 | // Table params generate for condition 13 | expect(Table.addParams({}, { conditions: [condition] })).toEqual({ 14 | ConditionExpression: '#n0 = #n1', 15 | ExpressionAttributeNames: { '#n0': 'name', '#n1': 'nickname' }, 16 | }); 17 | }); 18 | 19 | it('Condition.size', () => { 20 | const condition = Condition.eq(Condition.size('name'), 4); 21 | 22 | // Table params generate for condition 23 | expect(Table.addParams({}, { conditions: [condition] })).toEqual({ 24 | ConditionExpression: 'size(#n0) = :v0', 25 | ExpressionAttributeNames: { '#n0': 'name' }, 26 | ExpressionAttributeValues: { ':v0': 4 }, 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/test/Fields.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Fields', () => { 4 | require('../Fields'); 5 | }); 6 | 7 | it('Fields.compositeNamed', () => { 8 | require('../Fields.compositeNamed'); 9 | }); 10 | 11 | it('Fields.composite', () => { 12 | require('../Fields.composite'); 13 | }); 14 | 15 | it('Fields.split', () => { 16 | require('../Fields.split'); 17 | }); 18 | 19 | it('Fields.type', () => { 20 | require('../Fields.type'); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/test/Index.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Index', () => { 4 | require('../Index'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/test/KeyCondition.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('KeyCondition', () => { 4 | require('../KeyCondition'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/test/Model.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Model', () => { 4 | require('../Model'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/test/Readme.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Readme.Condition.ts', () => { 4 | require('../Readme.Condition'); 5 | }); 6 | 7 | it('Readme.Condition.Fields.ts', () => { 8 | require('../Readme.Condition.Fields'); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/test/Table.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Table.Simple.ts', () => { 4 | require('../Table.Simple'); 5 | }); 6 | 7 | it('Table.ts', () => { 8 | require('../Table'); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/test/Update.Examples.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports, jest/expect-expect */ 2 | 3 | it('Update.Model.ts', () => { 4 | require('../Update.Model'); 5 | }); 6 | 7 | it('Update.Table.ts', () => { 8 | require('../Update.Table'); 9 | }); 10 | -------------------------------------------------------------------------------- /inch.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "included": ["./dist/**/*.js"], 4 | "excluded": ["./dist/**/*.ts"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | testEnvironment: 'node', 8 | coveragePathIgnorePatterns: [ 9 | '/node_modules/', 10 | '/dist/', 11 | '/examples/', 12 | '/__test__/', 13 | ], 14 | watchman: true, 15 | coverageThreshold: { 16 | // Goal is 100% code coverage and add /* istanbul ignore next: [comment] */ where needed 17 | global: { 18 | branches: 100, 19 | functions: 100, 20 | lines: 100, 21 | statements: 100, 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-datamodel", 3 | "version": "0.2.7", 4 | "description": "DynamoDB single table design based model", 5 | "keywords": [ 6 | "DynamoDB", 7 | "Single-Table-Model", 8 | "aws", 9 | "nosql", 10 | "serverless" 11 | ], 12 | "author": "Jason Christensen", 13 | "license": "MIT", 14 | "homepage": "https://github.com/JasonCraftsCode/dynamodb-datamodel", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/JasonCraftsCode/dynamodb-datamodel.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/JasonCraftsCode/dynamodb-datamodel/issues" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "engines": { 29 | "node": ">=8.0.0" 30 | }, 31 | "sideEffects": false, 32 | "source": "src/index.ts", 33 | "types": "dist/index.d.ts", 34 | "main": "dist/index.js", 35 | "module": "dist/esm/index.js", 36 | "scripts": { 37 | "ncu": "ncu --dep prod,dev", 38 | "ncu:update": "ncu --dep prod,dev -u && npm install", 39 | "build": "tsc -P ./tsconfig.src.json", 40 | "build:esm": "tsc -P ./tsconfig.src.esm.json", 41 | "build:check": "tsc --noEmit -P ./tsconfig.src.json", 42 | "docs": "typedoc", 43 | "api": "api-extractor run", 44 | "api:update": "npm run build && api-extractor run --local --verbose", 45 | "docs:quality": "inchjs list --all", 46 | "format": "prettier --write \"src/**/*.ts\"", 47 | "lint": "eslint .", 48 | "test": "jest --no-cache --rootDir ./", 49 | "test:coverage": "jest --no-cache --rootDir ./ --coverage", 50 | "pr": "npm run lint && npm run test:coverage && npm run build && npm run build:esm && npm run api && npm run docs", 51 | "prepare": "npm run build && npm run build:esm", 52 | "prepublishOnly": "npm test && npm run lint", 53 | "preversion": "npm run lint", 54 | "version": "npm run format && git add -A src && npm run docs && git add -A docs", 55 | "postversion": "git push && git push --tags" 56 | }, 57 | "devDependencies": { 58 | "@microsoft/api-extractor": "^7.22.2", 59 | "@types/jest": "^27.4.1", 60 | "@types/node": "^17.0.24", 61 | "@typescript-eslint/eslint-plugin": "^5.19.0", 62 | "@typescript-eslint/parser": "^5.19.0", 63 | "aws-sdk": "^2.1116.0", 64 | "dependency-cruiser": "^11.6.0", 65 | "eslint": "^8.13.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-jest": "^26.1.4", 68 | "eslint-plugin-jsdoc": "^39.2.2", 69 | "eslint-plugin-prefer-arrow": "^1.2.3", 70 | "eslint-plugin-prettier": "^4.0.0", 71 | "eslint-plugin-tsdoc": "^0.2.16", 72 | "jest": "^27.5.1", 73 | "npm-check-updates": "^12.5.9", 74 | "prettier": "^2.6.2", 75 | "ts-jest": "^27.1.4", 76 | "ts-node": "^10.7.0", 77 | "typedoc": "^0.22.15", 78 | "typescript": "^4.6.3" 79 | }, 80 | "peerDependencies": { 81 | "aws-sdk": "^2.880.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ExpressionAttributes.ts: -------------------------------------------------------------------------------- 1 | import { ExpressionAttributeNameMap } from 'aws-sdk/clients/dynamodb'; 2 | import { Table } from './Table'; 3 | 4 | /** 5 | * Object used in Condition, KeyCondition and Update resolver methods to create attribute names and values 6 | * aliases and store the mappings for use in ExpressionAttributeNames and ExpressionAttributeValues params. 7 | * @public 8 | */ 9 | export class ExpressionAttributes implements Table.ExpressionAttributes { 10 | /** 11 | * RegEx that validates an attribute name would not need to use an alias. 12 | */ 13 | static validAttributeNameRegEx = /^[A-Za-z][A-Za-z0-9]*$/; 14 | 15 | /** 16 | * Validates that an attribute name can be used without an alias. 17 | * @param name - Name of an attribute. 18 | * @returns true if the attribute name is valid. 19 | */ 20 | static isValidAttributeName(name: string): boolean { 21 | return ExpressionAttributes.validAttributeNameRegEx.test(name); 22 | } 23 | 24 | /** 25 | * Property function to determine if the name is a reserved word and should use an alias. 26 | * For a current list of reserved words see DynamoDB-ReservedWords module in NPM. 27 | * The reason to set isReservedName and isValidName is to allow the attribute names to be directly 28 | * embedded into the expression string, which can make them easier to read. 29 | * @defaultValue '() =\> false;' - To use aliases for all attribute names. 30 | */ 31 | isReservedName: (name: string) => boolean = () => false; 32 | 33 | /** 34 | * Property function to determine if the name is valid to use without an alias. 35 | * Can use ExpressionAttribute.isValidAttributeName. 36 | * WARNING: Must be used with a proper isReservedName function to ensure reserved words are not used 37 | * without an alias otherwise the operations will return an error. 38 | * @defaultValue '() =\> false;' - To use aliases for all attribute names. 39 | */ 40 | isValidName: (name: string) => boolean = () => false; 41 | 42 | /** 43 | * Parse all names into paths by using the pathDelimiter, to make working with nested attributes easy. 44 | * @defaultValue true - Since most all names won't contain pathDelimiter then just do this by default. 45 | */ 46 | treatNameAsPath = true; 47 | 48 | /** 49 | * Delimiter to use for paths. 50 | * @defaultValue '.' - Period is used in javascript for nested objects. 51 | */ 52 | pathDelimiter = '.'; 53 | 54 | /** 55 | * Attribute names mapping, used to populate the ExpressionAttributeNames param. 56 | */ 57 | names: ExpressionAttributeNameMap = {}; 58 | 59 | /** 60 | * Auto incrementing name id used in names mapping. 61 | * @defaultValue 0 62 | */ 63 | nextName = 0; 64 | 65 | /** 66 | * Attribute values mapping, used to populate the ExpressionAttributeValues param. 67 | */ 68 | values: Table.AttributeValuesMap = {}; 69 | 70 | /** 71 | * Auto incrementing value id used in values mapping. 72 | * @defaultValue 0 73 | */ 74 | nextValue = 0; 75 | 76 | /** 77 | * Parse an attribute path and adds the names to the names mapping as needed and hands back an alias to use 78 | * in an expression. If the name already exists in the map the existing alias will be used. 79 | * When this.treatNameAsPath is true the name argument will be parsed as a path and will handle arrays 80 | * embedded in the path correctly, to allow access to all deep attribute. 81 | * @param name - Attribute path that can be delimited by a pathDelimiter and contain array notations '[]'. 82 | * @returns Alias path to use for the attribute name or the name if not aliasing is needed, delimited by '.'. 83 | */ 84 | addPath(name: string): string { 85 | if (this.treatNameAsPath) { 86 | const pathList = name.split(this.pathDelimiter).reduce((prev, curr) => { 87 | if (curr.endsWith(']')) { 88 | const beginBracket = curr.indexOf('['); 89 | const listName = this.addName(curr.substring(0, beginBracket)); 90 | prev.push(`${listName}${curr.substring(beginBracket)}`); 91 | } else prev.push(this.addName(curr)); 92 | 93 | return prev; 94 | }, new Array()); 95 | return pathList.join('.'); 96 | } 97 | return this.addName(name); 98 | } 99 | 100 | /** 101 | * Adds the value to the values map and hands back an alias to use in ab expression. 102 | * A new alias will always be created every time addValue is called. 103 | * @param value - Value to add to the values map. 104 | * @returns Alias to use in place of the value. 105 | */ 106 | addValue(value: Table.AttributeValues): string { 107 | const name = `:v${this.nextValue++}`; 108 | this.values[name] = value; 109 | return name; 110 | } 111 | 112 | /** 113 | * Gets the names map to assign to ExpressionAttributeNames. 114 | * @returns The map of all names added. 115 | */ 116 | getPaths(): ExpressionAttributeNameMap | void { 117 | if (Object.keys(this.names).length > 0) return this.names; 118 | } 119 | 120 | /** 121 | * Gets the values map to assign to ExpressionAttributeValues. 122 | * @returns The map of all values added. 123 | */ 124 | getValues(): Table.AttributeValuesMap | void { 125 | if (Object.keys(this.values).length > 0) return this.values; 126 | } 127 | 128 | /** 129 | * Helper method to set ExpressionAttributeNames and ExpressionAttributeValues values on the input argument. 130 | * @param params - Input params used for DocumentClient put, delete, update, query and scan methods. 131 | * @returns The input params argument passed in. 132 | */ 133 | static addParams(params: Table.ExpressionAttributeParams, attributes: Table.ExpressionAttributes): void { 134 | const paths = attributes.getPaths(); 135 | if (paths) params.ExpressionAttributeNames = paths; 136 | const values = attributes.getValues(); 137 | if (values) params.ExpressionAttributeValues = values; 138 | } 139 | 140 | /** 141 | * Private method to add an attribute name to the names mapping if needed and hand back an alias to use in 142 | * an expression. 143 | * @param name - Attribute name. 144 | * @returns Alias to use for the attribute name or the name if not aliasing is needed. 145 | */ 146 | private addName(name: string): string { 147 | const names = this.names; 148 | if (this.isReservedName(name)) { 149 | const attName = `#${name}`; 150 | names[attName] = name; 151 | return attName; 152 | } else if (!this.isValidName(name)) { 153 | for (const key in names) if (names[key] === name) return key; 154 | const attName = `#n${this.nextName++}`; 155 | names[attName] = name; 156 | return attName; 157 | } 158 | return name; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/TableValidate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-template-expressions */ 2 | import { Table } from './Table'; 3 | import { Index } from './TableIndex'; 4 | 5 | /** 6 | * Attribute names of the primary key for tables and indexes. 7 | */ 8 | export interface KeyName { 9 | /** 10 | * Partition key name. 11 | */ 12 | pk?: string; 13 | 14 | /** 15 | * Sort key name. 16 | */ 17 | sk?: string; 18 | } 19 | 20 | /** 21 | * Validate the key attributes for a {@link Table}. 22 | * @param ATTRIBUTES - The interface or type that has all required attributes, including table and index 23 | * primary key and all defined index projected attributes. 24 | * @param keyAttributes - Key attributes of the Table. 25 | * @param name - Name of Table. 26 | * @param onError - Method to call when there is a validation error. 27 | */ 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | export function validateKeyAttributes( 30 | keyAttributes: Table.PrimaryKey.AttributeTypesMapT, 31 | name: string, 32 | onError: (msg: string) => void, 33 | ): void { 34 | Object.keys(keyAttributes).forEach((key) => { 35 | const attr = keyAttributes[key] as { type: Table.PrimaryKey.AttributeTypes }; 36 | if (attr.type !== 'S' && attr.type !== 'N' && attr.type !== 'B') 37 | onError(`Primary key '${key}' has an invalid type of '${attr.type as string}' in table '${name}'`); 38 | }); 39 | } 40 | 41 | /** 42 | * Validate the key schema for a {@link Table}. 43 | * @param KEY - The interface of the table's primary key 44 | * @param ATTRIBUTES - The interface or type that has all required attributes, including table and index 45 | * primary key and all defined index projected attributes. 46 | * @param keySchema - Key schema of the Table. 47 | * @param keyAttributes - Key attributes of the Table. 48 | * @param name - Name of the Table. 49 | * @param onError - Method to call when there is a validation error. 50 | */ 51 | export function validateKeySchema< 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | KEY extends { [index: string]: any }, 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | ATTRIBUTES extends { [index: string]: any } = KEY, 56 | >( 57 | keySchema: Table.PrimaryKey.KeyTypesMapT, 58 | keyAttributes: Table.PrimaryKey.AttributeTypesMapT, 59 | name: string, 60 | onError: (msg: string) => void, 61 | ): KeyName { 62 | let { pk, sk }: KeyName = {}; 63 | Object.keys(keySchema).forEach((key) => { 64 | const schema = keySchema[key] as { keyType: Table.PrimaryKey.KeyTypes }; 65 | const attr = keyAttributes[key] as { type: Table.PrimaryKey.AttributeTypes }; 66 | if (attr === undefined) onError(`Key '${key}' not in table's keyAttributes`); 67 | 68 | if (schema.keyType === 'HASH') { 69 | if (pk !== undefined) onError(`Key '${key}' invalid, ${name} already has partition key '${pk}'`); 70 | pk = key; 71 | } else if (schema.keyType === 'RANGE') { 72 | if (sk !== undefined) onError(`Key '${key}' invalid, ${name} already has sort key '${sk}'`); 73 | sk = key; 74 | } else onError(`Key '${key}' has an invalid key type of '${schema.keyType as string}'`); 75 | }); 76 | if (pk === undefined) onError(`${name} needs partition key`); 77 | return { pk, sk }; 78 | } 79 | 80 | /** 81 | * Validates an {@link Index} for a {@link Table}. 82 | * @param index - An index for a Table. 83 | * @param names - Name of the other indexes. 84 | * @public 85 | */ 86 | export function validateIndex(index: Index, names?: Set): void { 87 | const { name, keySchema, projection, table, type } = index; 88 | const { keyAttributes, onError } = table; 89 | 90 | // Validate index name 91 | if (!name) onError(`Secondary index must have a name`); 92 | else if (names) { 93 | if (names.has(name)) onError(`Duplicate index name '${name}'`); 94 | names.add(name); 95 | } 96 | 97 | // Validate index projection 98 | const projType = projection.type; 99 | if (projType === 'INCLUDE') { 100 | if (!projection.attributes || projection.attributes.length <= 0) 101 | onError(`'${name}' projection type '${projType}' must have attributes`); 102 | } else if (projType !== 'ALL' && projType !== 'KEYS_ONLY') 103 | onError(`'${name}' projection type is invalidate '${projType as string}'`); 104 | else if (projection.attributes) onError(`${name}' projection type '${projType}' does not support attributes`); 105 | 106 | // Validate index keySchema 107 | const tableKey = { pk: table.getPartitionKey(), sk: table.getSortKey() }; 108 | const indexKey = validateKeySchema(keySchema, keyAttributes, name, onError); 109 | if (type === 'GLOBAL') { 110 | if (indexKey.pk === tableKey.pk && indexKey.sk === tableKey.sk) 111 | onError(`${name} global index has same partition key '${indexKey.pk}' and sort key '${indexKey.sk}' as table`); 112 | } else if (type === 'LOCAL') { 113 | if (indexKey.pk !== tableKey.pk) onError(`${name} partition key '${indexKey.pk}' needs to be '${tableKey.pk}'`); 114 | if (indexKey.sk === tableKey.sk) onError(`${name} has same sort key '${tableKey.sk}' as table`); 115 | if (indexKey.sk === undefined) onError(`${name} must have a sort key`); 116 | } else onError(`${name} index has invalid type: ${type}`); 117 | } 118 | 119 | /** 120 | * Validates an array of {@link Index} for a {@link Table}. 121 | * @param indexes - Indexes for a Table. 122 | * @public 123 | */ 124 | export function validateIndexes(indexes: Index[]): void { 125 | const names = new Set(); 126 | indexes.forEach((index) => validateIndex(index, names)); 127 | } 128 | 129 | /** 130 | * Validates that a {@link Table} is configured correctly. The Table's onError methods is called for any 131 | * validation errors. This method should primarily be used in tests to validate the table. 132 | * @param KEY - The interface of the table's primary key. 133 | * @param ATTRIBUTES - The interface or type that has all required attributes, including table and index primary 134 | * key and all defined index projected attributes. 135 | * @param table - Table to be validated. 136 | * @public 137 | */ 138 | export function validateTable(table: Table.TableT): void { 139 | const { name, keyAttributes, keySchema, onError } = table; 140 | if (!name) onError(`Table must have a name`); 141 | validateKeyAttributes(keyAttributes, name, onError); 142 | validateKeySchema(keySchema, keyAttributes, name, onError); 143 | } 144 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Condition, ConditionExpression } from './Condition'; 2 | export { ExpressionAttributes } from './ExpressionAttributes'; 3 | export { Fields } from './Fields'; 4 | export { KeyCondition, KeyConditionExpression } from './KeyCondition'; 5 | export { Model } from './Model'; 6 | export { Table } from './Table'; 7 | export { Index } from './TableIndex'; 8 | export { validateTable, validateIndex, validateIndexes } from './TableValidate'; 9 | export { Update, UpdateExpression } from './Update'; 10 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "examples", "__test__", ".eslintrc.js", "jest.config.js"], 4 | "exclude": ["node_modules", "coverage", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // same as es2015 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["es6"], // same as es2015 7 | "outDir": "dist", 8 | "baseUrl": "examples/baseUrl", // baseUrl used for examples so "import {Table} from 'dynamodb-datamodel'" can be used 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | // "sourceMap": true, 13 | // "declarationMap": true, 14 | "noErrorTruncation": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | // "noUnusedParameters": true, due to function params to enforce type checking 21 | "strict": true, 22 | "strictBindCallApply": true, 23 | "strictFunctionTypes": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true 26 | }, 27 | "exclude": ["/node_modules/", "/coverage/", "/dist/"], 28 | "typedocOptions": { 29 | "excludeExternals": true, 30 | "excludeInternal": true, 31 | "excludePrivate": true, 32 | "includeVersion": true, 33 | "entryPoints": "./src/index.ts", 34 | "out": "docs", 35 | "theme": "default", 36 | "gitRevision": "main", 37 | "includes": "./examples/" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.src.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "outDir": "dist/esm", 6 | "declaration": false, 7 | "declarationMap": false, 8 | "sourceMap": false, 9 | "removeComments": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": [ 5 | "node_modules", 6 | "coverage", 7 | "dist", 8 | "examples", 9 | "__test__", 10 | "**/*.spec.ts", 11 | "**/*.test.ts", 12 | "**/__test__/*" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------