├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── tests │ ├── index.test.ts │ ├── util-dynamodb.test.ts │ ├── util-map.test.ts │ ├── util-math.test.ts │ ├── util-time.test.ts │ └── util.test.ts ├── util-dynamodb.ts ├── util-map.ts ├── util-math.ts ├── util-time.ts └── util.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - name: Install 20 | run: yarn install 21 | - name: Test 22 | run: yarn test 23 | - name: Lint 24 | run: yarn lint 25 | 26 | publish-npm: 27 | if: github.event_name == 'push' # Push/merge only, not on PR 28 | needs: [test] 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: write 32 | contents: write 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v1 36 | with: 37 | node-version: 12 38 | - name: Install 39 | run: yarn install 40 | - name: Typescript build 41 | run: yarn build 42 | - name: Bump node version 43 | run: yarn bump 44 | - name: Publish to NPMJS 45 | uses: JS-DevTools/npm-publish@v1 46 | with: 47 | token: ${{ secrets.NPM_TOKEN }} 48 | package: package.json 49 | - name: Commit changes 50 | uses: stefanzweifel/git-auto-commit-action@v4.14.1 51 | with: 52 | commit_message: Bump version 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/cdk.out 4 | **/cdk.staging 5 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | src/ 3 | .cdk.staging 4 | cdk.out 5 | coverage 6 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Skyhook Adventure 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Appsync VTL Tester 2 | 3 | [![Built with 4 | typescript](https://badgen.net/badge/icon/typescript?icon=typescript&label)](https://www.typescriptlang.org/) 5 | [![version](https://badgen.net/npm/v/appsync-template-tester)](https://www.npmjs.com/package/appsync-template-tester) 6 | [![downloads](https://badgen.net/npm/dt/appsync-template-tester)](https://www.npmjs.com/package/appsync-template-tester) 7 | 8 | Write unit tests for AWS AppSync VTL resolvers, with popular frameworks such as Jest. 9 | 10 | ## Use 11 | 12 | ### Example 13 | 14 | ```shell 15 | yarn add appsync-template-tester --dev 16 | ``` 17 | 18 | ```typescript 19 | import Parser from 'appsync-template-tester'; 20 | import { readFileSync } from 'fs'; 21 | import { join } from 'path'; 22 | 23 | // Load from a file (if not in a string already) 24 | const templateFilePath = join(__dirname, './pathToFile.vtl'); 25 | const template = readFileSync(templateFilePath, { 26 | encoding: 'utf8', 27 | }); 28 | 29 | // Create the resolver 30 | const parser = new Parser(template); 31 | 32 | test('Test the resolver', () => { 33 | // The Appsync Context (ctx) object 34 | const context = { 35 | // For example with a dynamoDB response resolver: 36 | result: { 37 | id: 'testId', 38 | // ... 39 | }, 40 | }; 41 | 42 | // parser.resolve() automatically typecasts 43 | const response = parser.resolve(context); 44 | 45 | // For convenience, the response is returned as a JS object rather than JSON 46 | expect(response.id).toBe('testId'); 47 | }); 48 | ``` 49 | 50 | ### Util helpers supported 51 | 52 | This module supports all the provided core, map & time \$util methods, and most of the dynamodb methods. The underlying methods can be seen in the [Resolver Mapping Template Utility Reference 53 | docs](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference.html). 54 | 55 | Note: The errors list is also not returned (but \$util.error will throw an error). 56 | 57 | ### Extensions 58 | 59 | AWS AppSync provides extension methods via `$extensions`, for example `$extensions.evictFromApiCache`. It can be useful to assert that your VTL template is invoking these methods, therefore, you can provide custom extensions with your own implementations. Note that default extension methods are not provided. To read more about AWS AppSync extensions see the [Extensions 60 | docs](https://docs.aws.amazon.com/appsync/latest/devguide/extensions.html). 61 | 62 | ```javascript 63 | // Create the parser with the mock extension function 64 | const mockEvictFromApiCache = jest.fn(); 65 | const parser = new Parser(`$extensions.evictFromApiCache("Query", "users", { 66 | "context.arguments.id": $context.arguments.id 67 | })`); 68 | 69 | parser.resolve({ arguments: { id: 10 } }, undefined, { 70 | evictFromApiCache: mockEvictFromApiCache, 71 | }); 72 | 73 | expect(mockEvictFromApiCache).toHaveBeenCalledTimes(1) 74 | expect(mockEvictFromApiCache).toHaveBeenCalledWith("Query", "users", { "context.arguments.id": 10 }) 75 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-template-tester", 3 | "description": "Unit test AppSync VTL resolvers, with popular frameworks such as Jest", 4 | "keywords": [ 5 | "appsync", 6 | "aws", 7 | "template", 8 | "resolver", 9 | "apache", 10 | "velocity", 11 | "vtl", 12 | "unit", 13 | "test", 14 | "tester", 15 | "jest", 16 | "mocha", 17 | "jasmine", 18 | "velocityjs", 19 | "compile", 20 | "parse", 21 | "util" 22 | ], 23 | "repository": "https://github.com/alan-cooney/appsync-template-tester.git", 24 | "license": "MIT", 25 | "version": "1.1.19", 26 | "main": "dist/index.js", 27 | "scripts": { 28 | "build": "tsc --resolveJsonModule", 29 | "watch": "tsc -w --resolveJsonModule", 30 | "test": "./node_modules/.bin/jest", 31 | "coverage": "./node_modules/.bin/jest --collect-coverage", 32 | "lint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore", 33 | "bump": "./node_modules/.bin/versiony package.json --patch" 34 | }, 35 | "dependencies": { 36 | "moment-timezone": "^0.5.28", 37 | "uuid": "^8.0.0", 38 | "velocityjs": "^2.0.0" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^24.0.22", 42 | "@types/moment-timezone": "^0.5.13", 43 | "@types/node": "^13.13.5", 44 | "@types/uuid": "^7.0.3", 45 | "@typescript-eslint/eslint-plugin": "^2.27.0", 46 | "@typescript-eslint/parser": "^2.27.0", 47 | "eslint": "^6.8.0", 48 | "eslint-config-airbnb-typescript": "^7.2.1", 49 | "eslint-config-prettier": "^6.10.1", 50 | "eslint-plugin-import": "^2.20.2", 51 | "eslint-plugin-prettier": "^3.1.2", 52 | "jest": "^24.9.0", 53 | "prettier": "^2.0.4", 54 | "ts-jest": "^24.1.0", 55 | "typescript": "~3.7.2", 56 | "versiony-cli": "^1.3.0" 57 | }, 58 | "jest": { 59 | "testMatch": [ 60 | "**/*.test.ts" 61 | ], 62 | "transform": { 63 | "^.+\\.tsx?$": "ts-jest" 64 | }, 65 | "testEnvironment": "node", 66 | "coverageThreshold": { 67 | "global": { 68 | "branches": 90, 69 | "functions": 90, 70 | "lines": 90, 71 | "statements": 90 72 | } 73 | } 74 | }, 75 | "eslintConfig": { 76 | "root": true, 77 | "parser": "@typescript-eslint/parser", 78 | "plugins": [ 79 | "@typescript-eslint", 80 | "prettier" 81 | ], 82 | "extends": [ 83 | "airbnb-typescript/base", 84 | "prettier/@typescript-eslint", 85 | "plugin:prettier/recommended" 86 | ], 87 | "parserOptions": { 88 | "project": "./tsconfig.json", 89 | "ecmaVersion": 8, 90 | "sourceType": "module" 91 | }, 92 | "env": { 93 | "node": true 94 | }, 95 | "rules": { 96 | "prettier/prettier": "error", 97 | "no-new": "off", 98 | "no-console": "off", 99 | "import/prefer-default-export": "off" 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-classes-per-file 2 | import { render } from "velocityjs"; 3 | import * as utilCore from "./util"; 4 | import * as time from "./util-time"; 5 | import * as dynamodb from "./util-dynamodb"; 6 | import * as map from "./util-map"; 7 | import * as math from "./util-math"; 8 | 9 | export default class Parser { 10 | private template: string; 11 | 12 | private internalContext: Context; 13 | 14 | public get stash(): Record { 15 | return this.context?.stash ?? {}; 16 | } 17 | 18 | public get context(): Context { 19 | return this.internalContext ?? {}; 20 | } 21 | 22 | constructor(template: string) { 23 | this.template = template; 24 | } 25 | 26 | /** 27 | * Resolve as a string 28 | */ 29 | public resolve( 30 | context: Context, 31 | additionalUtil?: object, 32 | additionalExtensions?: object 33 | ): any { 34 | const clonedContext = JSON.parse(JSON.stringify(context)); 35 | if (!clonedContext.stash) clonedContext.stash = {}; 36 | clonedContext.args = clonedContext.arguments; 37 | 38 | const util = { 39 | ...utilCore, 40 | time, 41 | dynamodb, 42 | map, 43 | math, 44 | ...additionalUtil, 45 | }; 46 | 47 | const extensions = { ...additionalExtensions }; 48 | 49 | const params = { 50 | context: clonedContext, 51 | ctx: clonedContext, 52 | util, 53 | utils: util, 54 | extensions, 55 | }; 56 | 57 | const macros = { 58 | return(this: { stop(): void }, value: unknown | undefined) { 59 | this.stop(); 60 | return value !== undefined ? JSON.stringify(value) : "null"; 61 | }, 62 | }; 63 | 64 | const res = render(this.template, params, macros); 65 | 66 | // Keep the full context 67 | this.internalContext = clonedContext; 68 | 69 | // Remove preceding and trailing whitespace 70 | const resWithoutWhitespace = res 71 | .replace(/^[\n\s\r]*/, "") 72 | .replace(/[\n\s\r]*$/, ""); 73 | 74 | // Typecast Booleans 75 | if (res === "false") return false; 76 | if (res === "true") return true; 77 | 78 | // Typecast Null 79 | if (res === "null") return null; 80 | 81 | // Typecast Numbers 82 | // eslint-disable-next-line no-restricted-globals 83 | if (!isNaN((res as unknown) as number)) return parseFloat(res); 84 | 85 | // Typecast JSON to Object 86 | try { 87 | return JSON.parse(res); 88 | // eslint-disable-next-line no-empty 89 | } catch (e) {} 90 | 91 | // Return a string otherwise 92 | return resWithoutWhitespace; 93 | } 94 | } 95 | 96 | export type Context = { 97 | arguments?: object; 98 | source?: object; 99 | result?: object | string; 100 | identity?: object; 101 | request?: object; 102 | info?: object; 103 | error?: object; 104 | prev?: object; 105 | stash?: Record; 106 | }; 107 | 108 | export type velocityParams = { [blockName: string]: boolean }; 109 | -------------------------------------------------------------------------------- /src/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import Parser from "../index"; 2 | 3 | test("Simple vtl returns correctly", () => { 4 | const vtl = '$utils.toJson({"test": true})'; 5 | const parser = new Parser(vtl); 6 | const result = parser.resolve({}); 7 | expect(result).toEqual({ test: true }); 8 | }); 9 | 10 | test("util.qr hides result", () => { 11 | const vtl = ` 12 | #set($array = []) 13 | $util.qr($array.add(1)) 14 | {"test": $array}`; 15 | const parser = new Parser(vtl); 16 | const res = parser.resolve({}); 17 | expect(res).toEqual({ test: [1] }); 18 | // expect(res.includes('$util.qr($array.add("element in array"))')).toBeFalsy(); 19 | }); 20 | 21 | test("util.quiet hides result", () => { 22 | const vtl = ` 23 | #set($array = []) 24 | $util.quiet($array.add(1)) 25 | {"test": $array}`; 26 | const parser = new Parser(vtl); 27 | const res = parser.resolve({}); 28 | expect(res).toEqual({ test: [1] }); 29 | }); 30 | 31 | test("util.validate hides result if valid", () => { 32 | const vtl = ` 33 | $util.validate(true, "Error") 34 | {}`; 35 | const parser = new Parser(vtl); 36 | const res = parser.resolve({}); 37 | expect(res).toEqual({}); 38 | }); 39 | 40 | test("resolve with additional util", () => { 41 | const mockRdsToJsonObject = jest.fn(); 42 | const rdsResult = "rds result text"; 43 | mockRdsToJsonObject.mockImplementationOnce((args) => { 44 | return args === rdsResult ? [10] : []; 45 | }); 46 | const additionalUtil = { 47 | rds: { 48 | toJsonObject: mockRdsToJsonObject, 49 | }, 50 | }; 51 | const vtl = ` 52 | #set($response = $utils.rds.toJsonObject($ctx.result)[0]) 53 | {"test": $response}`; 54 | const parser = new Parser(vtl); 55 | const res = parser.resolve({ result: rdsResult }, additionalUtil); 56 | expect(res).toEqual({ test: 10 }); 57 | }); 58 | 59 | test("mocked extension is called", () => { 60 | // Create the parser with the mock extension function 61 | const mockEvictFromApiCache = jest.fn(); 62 | const parser = new Parser(`$extensions.evictFromApiCache("Query", "users", { 63 | "context.arguments.id": $context.arguments.id 64 | })`); 65 | 66 | parser.resolve({ arguments: { id: 10 } }, undefined, { 67 | evictFromApiCache: mockEvictFromApiCache, 68 | }); 69 | 70 | expect(mockEvictFromApiCache).toHaveBeenCalledTimes(1); 71 | expect(mockEvictFromApiCache).toHaveBeenCalledWith("Query", "users", { 72 | "context.arguments.id": 10, 73 | }); 74 | }); 75 | 76 | test("#return can return an object early", () => { 77 | const vtl = ` 78 | #return({"result": "A"}) 79 | {"result": "B"}`; 80 | const parser = new Parser(vtl); 81 | const res = parser.resolve({}); 82 | expect(res).toEqual({ result: "A" }); 83 | }); 84 | 85 | test("#return returns null if called without arguments", () => { 86 | const vtl = ` 87 | #return() 88 | {"result": "B"}`; 89 | const parser = new Parser(vtl); 90 | const res = parser.resolve({}); 91 | expect(res).toEqual(null); 92 | }); 93 | 94 | describe("$context keeps full context data", () => { 95 | describe("$context.stash keeps data", () => { 96 | test("Keep initial data", () => { 97 | const vtl = ""; 98 | const parser = new Parser(vtl); 99 | parser.resolve({ 100 | stash: { key: "value" }, 101 | }); 102 | expect(parser.stash).toStrictEqual({ key: "value" }); 103 | }); 104 | 105 | test("Keep resolved data", () => { 106 | const vtl = '$ctx.stash.put("key", "value")'; 107 | const parser = new Parser(vtl); 108 | parser.resolve({}); 109 | expect(parser.stash).toStrictEqual({ key: "value" }); 110 | }); 111 | 112 | test("Keep resolved data with complex structures", () => { 113 | const vtl = ` 114 | #set($nestedMap = {}) 115 | $util.qr($nestedMap.put("nestedKey", "nestedValue")) 116 | $ctx.stash.put("key", $nestedMap) 117 | `; 118 | const parser = new Parser(vtl); 119 | parser.resolve({}); 120 | expect(parser.stash).toStrictEqual({ key: { nestedKey: "nestedValue" } }); 121 | }); 122 | 123 | describe("Defaults if context is not defined", () => { 124 | test("Context defaults to an empty object", () => { 125 | const parser = new Parser(""); 126 | expect(parser.context).not.toBeUndefined(); 127 | expect(Object.keys(parser.context).length).toEqual(0); 128 | }); 129 | 130 | test("Stash defaults to an empty object", () => { 131 | const parser = new Parser(""); 132 | expect(parser.stash).not.toBeUndefined(); 133 | expect(Object.keys(parser.stash).length).toEqual(0); 134 | }); 135 | }); 136 | }); 137 | 138 | test("Keep argument modifications", () => { 139 | const vtl = ` 140 | #set($ctx.args.original = "bar") 141 | #set($ctx.args.addition = "value") 142 | `; 143 | const parser = new Parser(vtl); 144 | parser.resolve({ arguments: { original: "foo" } }); 145 | expect(parser.context.arguments).toStrictEqual({ 146 | original: "bar", 147 | addition: "value", 148 | }); 149 | }); 150 | }); 151 | 152 | describe("Typecasting works as expected", () => { 153 | test("Boolean false", () => { 154 | const vtl = "\nfalse "; // Note surrounding whitespace should be ignored 155 | const parser = new Parser(vtl); 156 | const res = parser.resolve({}); 157 | expect(res).toBe(false); 158 | }); 159 | 160 | test("Boolean true", () => { 161 | const vtl = "true"; 162 | const parser = new Parser(vtl); 163 | const res = parser.resolve({}); 164 | expect(res).toBe(true); 165 | }); 166 | 167 | test("Null", () => { 168 | const vtl = "null"; 169 | const parser = new Parser(vtl); 170 | const res = parser.resolve({}); 171 | expect(res).toBe(null); 172 | }); 173 | 174 | test("Integer", () => { 175 | const vtl = "123"; 176 | const parser = new Parser(vtl); 177 | const res = parser.resolve({}); 178 | expect(res).toBe(123); 179 | }); 180 | 181 | test("Float", () => { 182 | const vtl = "123.456"; 183 | const parser = new Parser(vtl); 184 | const res = parser.resolve({}); 185 | expect(res).toBe(123.456); 186 | }); 187 | 188 | test("JSON", () => { 189 | const vtl = '{"test": true}'; 190 | const parser = new Parser(vtl); 191 | const res = parser.resolve({}); 192 | expect(res).toEqual({ test: true }); 193 | }); 194 | 195 | test("Array", () => { 196 | const vtl = "[1,2,3]"; 197 | const parser = new Parser(vtl); 198 | const res = parser.resolve({}); 199 | expect(res).toEqual([1, 2, 3]); 200 | }); 201 | 202 | test("String", () => { 203 | const vtl = "abc123"; 204 | const parser = new Parser(vtl); 205 | const res = parser.resolve({}); 206 | expect(res).toBe("abc123"); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /src/tests/util-dynamodb.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | toBoolean, 3 | toBooleanJson, 4 | toDynamoDB, 5 | toList, 6 | toListJson, 7 | toMap, 8 | toMapJson, 9 | toMapValues, 10 | toMapValuesJson, 11 | toNumber, 12 | toNumberJson, 13 | toString, 14 | toStringJson, 15 | toStringSetJson, 16 | } from "../util-dynamodb"; 17 | 18 | /** 19 | * Example responses from: 20 | * https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference.html#dynamodb-helpers-in-util-dynamodb 21 | */ 22 | 23 | describe("string", () => { 24 | const i = "foo"; 25 | const expected = { S: "foo" }; 26 | 27 | test("toString", () => { 28 | const res = toString(i); 29 | expect(res).toEqual(expected); 30 | }); 31 | 32 | test("toStringJson", () => { 33 | const res = toStringJson(i); 34 | expect(JSON.parse(res)).toEqual(expected); 35 | }); 36 | 37 | test("toDynamoDB", () => { 38 | const res = toDynamoDB(i); 39 | expect(res).toEqual(expected); 40 | }); 41 | }); 42 | 43 | describe("number", () => { 44 | const i = 12345; 45 | const expected = { N: 12345 }; 46 | 47 | test("toNumber", () => { 48 | const res = toNumber(i); 49 | expect(res).toEqual(expected); 50 | }); 51 | 52 | test("toNumberJson", () => { 53 | const res = toNumberJson(i); 54 | expect(JSON.parse(res)).toEqual(expected); 55 | }); 56 | 57 | test("toDynamoDB", () => { 58 | const res = toDynamoDB(i); 59 | expect(res).toEqual(expected); 60 | }); 61 | }); 62 | 63 | describe("boolean", () => { 64 | const i = true; 65 | const expected = { BOOL: true }; 66 | 67 | test("toBoolean", () => { 68 | const res = toBoolean(i); 69 | expect(res).toEqual(expected); 70 | }); 71 | 72 | test("toBooleanJson", () => { 73 | const res = toBooleanJson(i); 74 | expect(JSON.parse(res)).toEqual(expected); 75 | }); 76 | 77 | test("toDynamoDB", () => { 78 | const res = toDynamoDB(i); 79 | expect(res).toEqual(expected); 80 | }); 81 | }); 82 | 83 | describe("list", () => { 84 | const i = ["foo", 123, { bar: "baz" }]; 85 | const expected = { 86 | L: [ 87 | { S: "foo" }, 88 | { N: 123 }, 89 | { 90 | M: { 91 | bar: { S: "baz" }, 92 | }, 93 | }, 94 | ], 95 | }; 96 | 97 | test("toList", () => { 98 | const res = toList(i); 99 | expect(res).toEqual(expected); 100 | }); 101 | 102 | test("toListJson", () => { 103 | const res = toListJson(i); 104 | expect(JSON.parse(res)).toEqual(expected); 105 | }); 106 | 107 | test("toDynamoDB", () => { 108 | const res = toDynamoDB(i); 109 | expect(res).toEqual(expected); 110 | }); 111 | }); 112 | 113 | describe("map", () => { 114 | const i = { foo: "bar", baz: 1234, beep: ["boop"] }; 115 | const expected = { 116 | M: { 117 | foo: { S: "bar" }, 118 | baz: { N: 1234 }, 119 | beep: { 120 | L: [{ S: "boop" }], 121 | }, 122 | }, 123 | }; 124 | 125 | test("toMap", () => { 126 | const res = toMap(i); 127 | expect(res).toEqual(expected); 128 | }); 129 | 130 | test("toMapJson", () => { 131 | const res = toMapJson(i); 132 | expect(JSON.parse(res)).toEqual(expected); 133 | }); 134 | 135 | test("toDynamoDB", () => { 136 | const res = toDynamoDB(i); 137 | expect(res).toEqual(expected); 138 | }); 139 | 140 | test("toMapValues", () => { 141 | const res = toMapValues(i); 142 | expect(res).toEqual(expected.M); 143 | }); 144 | 145 | test("toMapValuesJson", () => { 146 | const res = toMapValuesJson(i); 147 | expect(JSON.parse(res)).toEqual(expected.M); 148 | }); 149 | 150 | test("toStringSetJson", () => { 151 | const input = ["a", "b", "c"]; 152 | const res = toStringSetJson(input); 153 | expect(res).toStrictEqual({ SS: ["a", "b", "c"] }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/tests/util-map.test.ts: -------------------------------------------------------------------------------- 1 | import { copyAndRemoveAllKeys, copyAndRetainAllKeys } from "../util-map"; 2 | 3 | test("copyAndRemoveAllKeys", () => { 4 | const map = { 5 | one: 1, 6 | two: 2, 7 | three: 3, 8 | }; 9 | 10 | const list = ["one", "two"]; 11 | 12 | const expected = { 13 | three: 3, 14 | }; 15 | 16 | expect(copyAndRemoveAllKeys(map, list)).toMatchObject(expected); 17 | }); 18 | 19 | test("copyAndRetailAllKeys", () => { 20 | const map = { 21 | one: 1, 22 | two: 2, 23 | three: 3, 24 | }; 25 | 26 | const list = ["one", "two"]; 27 | 28 | const expected = { 29 | one: 1, 30 | two: 2, 31 | }; 32 | 33 | expect(copyAndRetainAllKeys(map, list)).toMatchObject(expected); 34 | }); 35 | -------------------------------------------------------------------------------- /src/tests/util-math.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | roundNum, 3 | minVal, 4 | maxVal, 5 | randomDouble, 6 | randomWithinRange, 7 | } from "../util-math"; 8 | 9 | describe("util-math", () => { 10 | describe("roundNum()", () => { 11 | it("returns the value of a number rounded to the nearest integer", () => { 12 | expect(roundNum(-0.6)).toBe(-1); 13 | expect(roundNum(-0)).toBe(-0); 14 | expect(roundNum(0)).toBe(0); 15 | expect(roundNum(0.6)).toBe(1); 16 | expect(roundNum(2.4)).toBe(2); 17 | expect(roundNum(3)).toBe(3); 18 | }); 19 | }); 20 | 21 | describe("minVal()", () => { 22 | it("returns the smallest of the numbers given as input parameters", () => { 23 | expect(minVal(-0.4, -0.2)).toBe(-0.4); 24 | expect(minVal(-0, 0)).toBe(-0); 25 | expect(minVal(0, 0)).toBe(0); 26 | expect(minVal(-5, 2)).toBe(-5); 27 | expect(minVal(5, 2)).toBe(2); 28 | }); 29 | 30 | it("returns 0 if there are no parameters", () => { 31 | expect(minVal()).toBe(0); 32 | }); 33 | }); 34 | 35 | describe("maxVal()", () => { 36 | it("returns the largest of the numbers given as input parameters", () => { 37 | expect(maxVal(-0.4, -0.2)).toBe(-0.2); 38 | expect(maxVal(-0, 0)).toBe(0); 39 | expect(maxVal(-5, 2)).toBe(2); 40 | expect(maxVal(5, 2)).toBe(5); 41 | }); 42 | 43 | it("return 0 if there are no parameters", () => { 44 | expect(maxVal()).toBe(0); 45 | }); 46 | }); 47 | 48 | describe("randomDouble()", () => { 49 | Array.from(Array(50), () => randomDouble()).forEach( 50 | (randomNum: number, index, arr) => { 51 | const isWithinRange = randomNum > 0 && randomNum < 1; 52 | const isUnique = arr.lastIndexOf(randomNum) === index; 53 | 54 | it(`${randomNum} is larger than 0 and less than 1`, () => { 55 | expect(isWithinRange).toBe(true); 56 | }); 57 | 58 | it(`${randomNum} did not result more than once`, () => { 59 | expect(isUnique).toBe(true); 60 | }); 61 | } 62 | ); 63 | }); 64 | 65 | describe("randomWithinRange()", () => { 66 | const MIN = 25; 67 | const MAX = 4548412; 68 | Array.from(Array(50), () => randomWithinRange(MIN, MAX)).forEach( 69 | (randomNum: number, index, arr) => { 70 | const isWithinRange = randomNum > MIN && randomNum < MAX; 71 | const isUnique = arr.lastIndexOf(randomNum) === index; 72 | 73 | it(`${randomNum} is larger than ${MIN} and less than ${MAX}`, () => { 74 | expect(isWithinRange).toBe(true); 75 | }); 76 | 77 | it(`${randomNum} did not result more than once`, () => { 78 | expect(isUnique).toBe(true); 79 | }); 80 | } 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/tests/util-time.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | nowISO8601, 3 | nowEpochSeconds, 4 | nowEpochMilliSeconds, 5 | nowFormatted, 6 | parseFormattedToEpochMilliSeconds, 7 | parseISO8601ToEpochMilliSeconds, 8 | epochMilliSecondsToSeconds, 9 | epochMilliSecondsToISO8601, 10 | epochMilliSecondsToFormatted, 11 | } from "../util-time"; 12 | 13 | test("nowISO8601", () => { 14 | const res = nowISO8601(); 15 | expect(res).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/); 16 | }); 17 | 18 | test("nowEpochSeconds", () => { 19 | const res = nowEpochSeconds(); 20 | expect(res.toString().length).toBe(10); 21 | }); 22 | 23 | test("nowEpochMilliSeconds", () => { 24 | const res = nowEpochMilliSeconds(); 25 | expect(res.toString().length).toBe(13); 26 | }); 27 | 28 | describe("nowFormatted", () => { 29 | test("format only", () => { 30 | const res = nowFormatted("yyyy-MM-dd HH:mm:ssZ"); 31 | expect(res).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+\d{4}$/); 32 | }); 33 | 34 | test("format and timezone", () => { 35 | const res = nowFormatted("yyyy-MM-dd HH:mm:ssZ", "Australia/Perth"); 36 | expect(res).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+0800$/); 37 | }); 38 | }); 39 | 40 | describe("parseFormattedToEpochMilliSeconds", () => { 41 | test("time and format", () => { 42 | const format = "yyyy-MM-ddTHH:mm:ssZ"; 43 | const time = nowFormatted(format); 44 | const res = parseFormattedToEpochMilliSeconds(time, format); 45 | expect(res.toString().length).toBe(13); 46 | }); 47 | 48 | test("time and format and timezone", () => { 49 | const format = "yyyy-MM-ddTHH:mm:ssZ"; 50 | const time = nowFormatted(format); 51 | const res = parseFormattedToEpochMilliSeconds( 52 | time, 53 | format, 54 | "Australia/Perth" 55 | ); 56 | expect(res.toString().length).toBe(13); 57 | }); 58 | }); 59 | 60 | test("parseISO8601ToEpochMilliSeconds", () => { 61 | const time = "2018-02-01T17:21:05.180+08:00"; 62 | const res = parseISO8601ToEpochMilliSeconds(time); 63 | expect(res.toString().length).toBe(13); 64 | }); 65 | 66 | test("epochMilliSecondsToSeconds", () => { 67 | const time = 1517943695750; 68 | const expected = 1517943695; 69 | const res = epochMilliSecondsToSeconds(time); 70 | expect(res).toBe(expected); 71 | }); 72 | 73 | test("epochMilliSecondsToISO8601", () => { 74 | const time = 1517943695750; 75 | const expected = "2018-02-06T19:01:35.750Z"; 76 | const res = epochMilliSecondsToISO8601(time); 77 | expect(res).toBe(expected); 78 | }); 79 | 80 | describe("epochMilliSecondsToFormatted", () => { 81 | test("time & format", () => { 82 | const time = 1417943695859; 83 | const format = "yyyy-MM-dd HH:mm:ssZ"; 84 | const expected = "2014-12-07 00:00:00+0000"; 85 | const res = epochMilliSecondsToFormatted(time, format); 86 | expect(res).toBe(expected); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/tests/util.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | escapeJavaScript, 3 | qr, 4 | quiet, 5 | urlEncode, 6 | urlDecode, 7 | base64Encode, 8 | base64Decode, 9 | parseJson, 10 | toJson, 11 | autoId, 12 | unauthorized, 13 | error, 14 | appendError, 15 | validate, 16 | isNull, 17 | isNullOrEmpty, 18 | isNullOrBlank, 19 | defaultIfNull, 20 | defaultIfNullOrEmpty, 21 | defaultIfNullOrBlank, 22 | isString, 23 | isNumber, 24 | isBoolean, 25 | isList, 26 | isMap, 27 | typeOf, 28 | matches, 29 | } from "../util"; 30 | 31 | /** 32 | * Tests are predominantly generated by running these objects through an AppSync resolver tester (on a real resolver) 33 | */ 34 | 35 | test("qr returns blank string", () => { 36 | const res = qr(); 37 | expect(res).toBe(""); 38 | }); 39 | 40 | test("quiet returns blank string", () => { 41 | const res = quiet(); 42 | expect(res).toBe(""); 43 | }); 44 | 45 | test("escapeJavaScrip escapes JavaScript (not necessarily in exactly the same way as AppSync)", () => { 46 | const str = "' ? ! ` /"; 47 | const res = escapeJavaScript(str); 48 | expect(res).not.toBe(str); 49 | }); 50 | 51 | test("urlEncode encodes URLs in the same way as AppSync", () => { 52 | const str = "/hi?page=1&test=2"; 53 | const expected = "%2Fhi%3Fpage%3D1%26test%3D2"; 54 | const res = urlEncode(str); 55 | expect(res).toBe(expected); 56 | }); 57 | 58 | test("urlDecode decodes URLs in the same way as AppSync", () => { 59 | const str = "%2Fhi%3Fpage%3D1%26test%3D2"; 60 | const expected = "/hi?page=1&test=2"; 61 | const res = urlDecode(str); 62 | expect(res).toBe(expected); 63 | }); 64 | 65 | test("base64Encode encodes in the same way as AppSync", () => { 66 | const str = "hello()"; 67 | const expected = "aGVsbG8oKQ=="; 68 | const res = base64Encode(str); 69 | expect(res).toBe(expected); 70 | }); 71 | 72 | test("base64Decode decodes in the same way as AppSync", () => { 73 | const buffer = "aGVsbG8oKQ=="; 74 | const expected = "hello()"; 75 | const res = base64Decode(buffer); 76 | expect(res).toBe(expected); 77 | }); 78 | 79 | test("parseJson parses a JSON string and returns the object", () => { 80 | const obj = { 81 | testKey: "testValue", 82 | }; 83 | const json = JSON.stringify(obj); 84 | const res = parseJson(json); 85 | expect(res).toEqual(obj); 86 | }); 87 | 88 | test("toJson stringifies an object", () => { 89 | const obj = { 90 | testKey: "testValue", 91 | }; 92 | const json = JSON.stringify(obj); 93 | const res = toJson(obj); 94 | expect(res).toBe(json); 95 | }); 96 | 97 | test("toJson stringifies an string", () => { 98 | const obj = "testValue"; 99 | const json = JSON.stringify(obj); 100 | const res = toJson(obj); 101 | expect(res).toBe(json); 102 | }); 103 | 104 | test("autoId returns a 128-bit ID", () => { 105 | const res = autoId(); 106 | const expectedFormat = /^[\w\d]{8}-[\w\d]{4}-[\w\d]{4}-[\w\d]{4}-[\w\d]{12}$/; 107 | expect(res).toMatch(expectedFormat); 108 | }); 109 | 110 | test("unauthorized", () => { 111 | expect(() => unauthorized()).toThrow("Unauthorized"); 112 | }); 113 | 114 | describe("error", () => { 115 | test("throws first string argument if given", () => { 116 | const testMessage = "errorMessage"; 117 | expect(() => error(testMessage)).toThrow(testMessage); 118 | }); 119 | 120 | test("does nothing with no arguments", () => { 121 | error(); 122 | }); 123 | }); 124 | 125 | test("appendError does nothing", () => { 126 | appendError(); 127 | }); 128 | 129 | describe("validate", () => { 130 | test("false throws an error", () => { 131 | const testMessage = "errorMessage"; 132 | expect(() => validate(false, testMessage)).toThrow(testMessage); 133 | }); 134 | 135 | test("true returns a blank string", () => { 136 | const result = validate(true, "errorMessage"); 137 | expect(result).toBe(""); 138 | }); 139 | }); 140 | 141 | describe("isNull", () => { 142 | test.each([ 143 | [null, true], 144 | [undefined, true], 145 | ["", false], 146 | ["\t", false], 147 | ["sdfasdf", false], 148 | ])("isNull(%p) -> %p", (input, expected) => { 149 | const res = isNull(input); 150 | expect(res).toBe(expected); 151 | }); 152 | }); 153 | 154 | describe("isNullOrEmpty", () => { 155 | test.each([ 156 | [null, true], 157 | [undefined, true], 158 | ["", true], 159 | ["\t", false], 160 | ["sdfasdf", false], 161 | ])("isNullOrEmpty(%p) -> %p", (input, expected) => { 162 | const res = isNullOrEmpty(input); 163 | expect(res).toBe(expected); 164 | }); 165 | }); 166 | 167 | describe("isNullOrBlank", () => { 168 | test.each([ 169 | [null, true], 170 | [undefined, true], 171 | ["", true], 172 | ["\t", true], 173 | ["sdfasdf", false], 174 | ])("isNullOrBlank(%p) -> %p", (input, expected) => { 175 | const res = isNullOrBlank(input); 176 | expect(res).toBe(expected); 177 | }); 178 | }); 179 | 180 | describe("defaultIfNull", () => { 181 | test.each([ 182 | [null, true], 183 | ["", false], 184 | ["\t", false], 185 | ["sdfasdf", false], 186 | ])("defaultIfNull(%p) -> %p", (obj, expectShowDefault) => { 187 | const defaultObj = "default"; 188 | const res = defaultIfNull(obj, defaultObj); 189 | const expected = expectShowDefault ? defaultObj : obj; 190 | expect(res).toBe(expected); 191 | }); 192 | }); 193 | 194 | describe("defaultIfNullOrEmpty", () => { 195 | test.each([ 196 | [null, true], 197 | ["", true], 198 | ["\t", false], 199 | ["sdfasdf", false], 200 | ])("defaultIfNullOrEmpty(%p) -> %p", (obj, expectShowDefault) => { 201 | const defaultObj = "default"; 202 | const res = defaultIfNullOrEmpty(obj, defaultObj); 203 | const expected = expectShowDefault ? defaultObj : obj; 204 | expect(res).toBe(expected); 205 | }); 206 | }); 207 | 208 | describe("defaultIfNullOrBlank", () => { 209 | test.each([ 210 | [null, true], 211 | ["", true], 212 | ["\t", true], 213 | ["sdfasdf", false], 214 | ])("defaultIfNullOrBlank(%p) -> %p", (obj, expectShowDefault) => { 215 | const defaultObj = "default"; 216 | const res = defaultIfNullOrBlank(obj, defaultObj); 217 | const expected = expectShowDefault ? defaultObj : obj; 218 | expect(res).toBe(expected); 219 | }); 220 | }); 221 | 222 | describe("type checking", () => { 223 | test.each` 224 | value | isStr | isNum | isBool | isLst | isMp | typ 225 | ${null} | ${false} | ${false} | ${false} | ${false} | ${false} | ${"Null"} 226 | ${"abc"} | ${true} | ${false} | ${false} | ${false} | ${false} | ${"String"} 227 | ${""} | ${true} | ${false} | ${false} | ${false} | ${false} | ${"String"} 228 | ${1} | ${false} | ${true} | ${false} | ${false} | ${false} | ${"Number"} 229 | ${0} | ${false} | ${true} | ${false} | ${false} | ${false} | ${"Number"} 230 | ${false} | ${false} | ${false} | ${true} | ${false} | ${false} | ${"Boolean"} 231 | ${true} | ${false} | ${false} | ${true} | ${false} | ${false} | ${"Boolean"} 232 | ${[]} | ${false} | ${false} | ${false} | ${true} | ${false} | ${"List"} 233 | ${{}} | ${false} | ${false} | ${false} | ${false} | ${true} | ${"Map"} 234 | `( 235 | "the type for value %p is detected correctly", 236 | ({ value, isStr, isNum, isBool, isLst, isMp, typ }) => { 237 | expect(isString(value)).toBe(isStr); 238 | expect(isNumber(value)).toBe(isNum); 239 | expect(isBoolean(value)).toBe(isBool); 240 | expect(isList(value)).toBe(isLst); 241 | expect(isMap(value)).toBe(isMp); 242 | expect(typeOf(value)).toBe(typ); 243 | } 244 | ); 245 | }); 246 | 247 | test("matches", () => { 248 | const res = matches("^a", "a"); 249 | expect(res).toBeTruthy(); 250 | }); 251 | -------------------------------------------------------------------------------- /src/util-dynamodb.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | /* eslint-disable import/prefer-default-export */ 3 | import { isBoolean, isList, isNumber, isString } from "./util"; 4 | 5 | export function toString(str: string) { 6 | return { S: str }; 7 | } 8 | 9 | export function toStringJson(str: string) { 10 | return JSON.stringify(toString(str)); 11 | } 12 | 13 | export function toNumber(num: number) { 14 | return { N: num }; 15 | } 16 | 17 | export function toNumberJson(num: number) { 18 | return JSON.stringify(toNumber(num)); 19 | } 20 | 21 | export function toBoolean(bool: boolean) { 22 | return { BOOL: bool }; 23 | } 24 | 25 | export function toBooleanJson(bool: boolean) { 26 | return JSON.stringify(toBoolean(bool)); 27 | } 28 | 29 | export function toList(list: Array) { 30 | return { 31 | L: list.map((item: any) => toDynamoDB(item)), 32 | }; 33 | } 34 | 35 | export function toListJson(list: Array) { 36 | return JSON.stringify(toList(list)); 37 | } 38 | 39 | export function toMap(map: any) { 40 | const obj = JSON.parse(JSON.stringify(map)); 41 | // eslint-disable-next-line array-callback-return 42 | Object.keys(obj).map((key) => { 43 | obj[key] = toDynamoDB(obj[key]); 44 | }); 45 | 46 | return { M: obj }; 47 | } 48 | 49 | export function toMapJson(map: any) { 50 | return JSON.stringify(toMap(map)); 51 | } 52 | 53 | export function toDynamoDB(i: any): any { 54 | if (isString(i)) { 55 | return toString(i); 56 | } 57 | 58 | if (isNumber(i)) { 59 | return toNumber(i); 60 | } 61 | 62 | if (isBoolean(i)) { 63 | return toBoolean(i); 64 | } 65 | 66 | if (isList(i)) { 67 | return toList(i); 68 | } 69 | 70 | return toMap(i); 71 | } 72 | 73 | export function toDynamoDBJson(i: any) { 74 | return JSON.stringify(toDynamoDB(i)); 75 | } 76 | 77 | export function toStringSetJson(list: Array): { SS: Array } { 78 | return { SS: list }; 79 | } 80 | 81 | /** 82 | * The same as toMapJSON but only for the properties (the root obect becomes {a: {S: "s"}} rather than {M: {a: {S: "s"}}}) 83 | */ 84 | export function toMapValues(map: any) { 85 | const obj = JSON.parse(JSON.stringify(map)); 86 | // eslint-disable-next-line array-callback-return 87 | Object.keys(obj).map((key) => { 88 | obj[key] = toDynamoDB(obj[key]); 89 | }); 90 | return obj; 91 | } 92 | 93 | export function toMapValuesJson(i: any) { 94 | return JSON.stringify(toMapValues(i)); 95 | } 96 | -------------------------------------------------------------------------------- /src/util-map.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | /** 3 | * Map helpers in $util.map 4 | * 5 | * $util.map contains methods to help with common 6 | * Map operations, such as removing or retaining 7 | * items from a Map for filtering use cases. 8 | */ 9 | 10 | /** 11 | * Makes a shallow copy of the first map, retaining only the 12 | * keys specified in the list, if they are present. All other 13 | * keys will be removed from the copy. 14 | * @param map 15 | * @param list 16 | */ 17 | export function copyAndRetainAllKeys(map: object, list: Array): object { 18 | return Object.entries(map).reduce((result, [key, value]) => { 19 | if (list.indexOf(key) === -1) return result; 20 | return { 21 | ...result, 22 | [key]: value, 23 | }; 24 | }, {}); 25 | } 26 | 27 | /** 28 | * Makes a shallow copy of the first map, removing any entries 29 | * where the key is specified in the list, if they are present. 30 | * All other keys will be retained in the copy. 31 | * @param map 32 | * @param list 33 | */ 34 | export function copyAndRemoveAllKeys(map: object, list: Array): object { 35 | const result: any = { ...map }; 36 | list.forEach((key) => delete result[key]); 37 | return result; 38 | } 39 | -------------------------------------------------------------------------------- /src/util-math.ts: -------------------------------------------------------------------------------- 1 | export function roundNum(num?: number) { 2 | return Math.round(num ?? 0); 3 | } 4 | 5 | export function minVal(numA?: number, numB?: number) { 6 | return Math.min(numA ?? 0, numB ?? 0); 7 | } 8 | 9 | export function maxVal(numA?: number, numB?: number) { 10 | return Math.max(numA ?? 0, numB ?? 0); 11 | } 12 | 13 | export function randomDouble() { 14 | return Math.random(); 15 | } 16 | 17 | export function randomWithinRange(min: number, max: number) { 18 | return Math.floor(min + Math.random() * (max - min + 1)); 19 | } 20 | -------------------------------------------------------------------------------- /src/util-time.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import moment from "moment-timezone"; 3 | 4 | /** 5 | * Helper function to convert the format string from the vtl format to the moment format 6 | */ 7 | function vtlToMomentFormat(format?: string) { 8 | return format?.replace("dd", "DD").replace("Z", "ZZ"); 9 | } 10 | 11 | export function nowISO8601() { 12 | return moment.utc().toISOString(); 13 | } 14 | 15 | export function nowEpochSeconds() { 16 | return moment().unix(); 17 | } 18 | 19 | export function nowEpochMilliSeconds() { 20 | return moment().valueOf(); 21 | } 22 | 23 | export function nowFormatted(format: string, timezone: string = "utc") { 24 | const vtlFormatConverted = vtlToMomentFormat(format); 25 | return moment().tz(timezone).format(vtlFormatConverted); 26 | } 27 | 28 | export function parseFormattedToEpochMilliSeconds( 29 | time: string, 30 | formatFrom: string, 31 | timezone: string = "utc" 32 | ) { 33 | const reverseFormat = vtlToMomentFormat(formatFrom); 34 | // AppSync does not parse in strict mode 35 | return moment(time, reverseFormat).tz(timezone).valueOf(); 36 | } 37 | 38 | export function parseISO8601ToEpochMilliSeconds(time: string) { 39 | // AppSync does not parse in strict mode 40 | return moment(time, "YYYY-MM-DDTHH:mm:ssZ").valueOf(); 41 | } 42 | 43 | export function epochMilliSecondsToSeconds(time: number) { 44 | return moment(time).unix(); 45 | } 46 | 47 | export function epochMilliSecondsToISO8601(time: number) { 48 | return moment(time).utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]"); 49 | } 50 | 51 | export function epochMilliSecondsToFormatted( 52 | time: number, 53 | format?: string, 54 | timezone: string = "utc" 55 | ) { 56 | const vtlFormatConverted = vtlToMomentFormat(format); 57 | return moment(time).tz(timezone).format(vtlFormatConverted); 58 | } 59 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { v4 } from "uuid"; 3 | 4 | export function qr() { 5 | return ""; 6 | } 7 | 8 | export function quiet() { 9 | return ""; 10 | } 11 | 12 | export function escapeJavaScript(code: string) { 13 | // Appsync actually has it's own escape functionality which handles some characters differently 14 | return encodeURI(code); 15 | } 16 | 17 | export function urlEncode(url: string) { 18 | // Appsync also encodes the / character 19 | return escape(url).replace("/", "%2F"); 20 | } 21 | 22 | export function urlDecode(url: string) { 23 | // Appsync also decodes the / character 24 | const urlWithSlash = url.replace("%2F", "/"); 25 | return unescape(urlWithSlash); 26 | } 27 | 28 | export function base64Encode(data: string) { 29 | return Buffer.from(data).toString("base64"); 30 | } 31 | 32 | export function base64Decode(buffer: string) { 33 | return Buffer.from(buffer, "base64").toString("ascii"); 34 | } 35 | 36 | export function parseJson(json: string) { 37 | return JSON.parse(json); 38 | } 39 | 40 | export function toJson(obj: Object) { 41 | return JSON.stringify(obj); 42 | } 43 | 44 | export function autoId() { 45 | return v4(); 46 | } 47 | 48 | export function unauthorized() { 49 | throw Error("Unauthorized"); 50 | } 51 | 52 | export function error(message?: string) { 53 | // Whilst this function takes up to 4 inputs, it only throws the first input in the AWS AppSync resolver tester 54 | if (message) { 55 | throw Error(message); 56 | } 57 | } 58 | 59 | export function appendError() { 60 | // Does nothing as side-effects can't be handled by velocityjs - in practice this would add items to the errors array 61 | } 62 | 63 | export function validate(bool: Boolean, message?: string) { 64 | if (!bool) { 65 | throw new Error(message); 66 | } 67 | return ""; 68 | } 69 | 70 | export function isNull(input?: any) { 71 | // Technically undefined returns null, however the javascript VTL tool then gets confused when this is used in 72 | // conditionals (the most common use case) so we'll keep the simple approach here. 73 | return input === null || typeof input === "undefined"; 74 | } 75 | 76 | export function isNullOrEmpty(input?: any) { 77 | return !input; 78 | } 79 | 80 | export function isNullOrBlank(input?: any) { 81 | if (!input) { 82 | return true; 83 | } 84 | if (typeof input === "string" && input.match(/^[\n\t\r\s]*$/)) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | export function defaultIfNull(obj: any, defaultObj: any) { 91 | if (obj === null || typeof obj === "undefined") { 92 | return defaultObj; 93 | } 94 | return obj; 95 | } 96 | 97 | export function defaultIfNullOrEmpty(obj: any, defaultObj: any) { 98 | if (!obj) { 99 | return defaultObj; 100 | } 101 | return obj; 102 | } 103 | 104 | export function defaultIfNullOrBlank(obj: any, defaultObj: any) { 105 | if (!obj) { 106 | return defaultObj; 107 | } 108 | if (typeof obj === "string" && obj.match(/^[\n\t\r\s]*$/)) { 109 | return defaultObj; 110 | } 111 | return obj; 112 | } 113 | 114 | export function isString(i: any) { 115 | return typeof i === "string"; 116 | } 117 | 118 | export function isNumber(i: any) { 119 | return typeof i === "number"; 120 | } 121 | 122 | export function isBoolean(i: any) { 123 | return typeof i === "boolean"; 124 | } 125 | 126 | export function isList(i: any) { 127 | return Array.isArray(i); 128 | } 129 | 130 | export function isMap(i: any) { 131 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object for details as to why 132 | // this works 133 | return i === Object(i) && !Array.isArray(i) && typeof i === "object"; 134 | } 135 | 136 | export function typeOf(i: any) { 137 | if (isNull(i)) { 138 | return "Null"; 139 | } 140 | if (isNumber(i)) { 141 | return "Number"; 142 | } 143 | if (isString(i)) { 144 | return "String"; 145 | } 146 | if (isMap(i)) { 147 | return "Map"; 148 | } 149 | if (isList(i)) { 150 | return "List"; 151 | } 152 | if (isBoolean(i)) { 153 | return "Boolean"; 154 | } 155 | return "Object"; 156 | } 157 | 158 | export function matches(patternString: string, str: string) { 159 | const pattern = new RegExp(patternString); 160 | return !!str.match(pattern); 161 | } 162 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "target": "ES2018", 6 | "module": "commonjs", 7 | "lib": ["es2018", "dom"], 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "resolveJsonModule": true, 24 | "esModuleInterop": true 25 | } 26 | } 27 | --------------------------------------------------------------------------------