{"_id":"dynamo-arc","_rev":"34-1373fe1f0e1f04f00f875de236d427de","name":"dynamo-arc","dist-tags":{"latest":"2.2.1","beta":"2.2.0-1"},"versions":{"1.0.0":{"name":"dynamo-arc","version":"1.0.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"https://github.nike.com/sprockets/node-dynamo.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"7b26ca0c87f0c42ad01de3ec7fdb9fbd11988755","_id":"dynamo-arc@1.0.0","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-liowvuta+osNJfJFhJe8n6zok9j4kUEVzLB0CzTJ2MsmNKm5tM7k5/l3P28VHaGAKpQ5h3HXp55DqX+mIUrdkQ==","shasum":"be8a88de48f6e58e158086adea83bcef191ef775","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.0.0.tgz","fileCount":8,"unpackedSize":28591,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfUFSyCRA9TVsSAnZWagAA/9MQAJRZArlK+YflsmG9nT0Y\nQQK/1wrRUPB4hSARWyAlNOW65wN6c/hjH6TdtUfT8OXTjgCObucZuPYdRpS2\nIwuTHtGe3LcHBYCgbswGz8EOMtrZIRBwVjQpgbAiGpJwlwE8uPCF+BW/tZ/w\nqH7LbefKTU/Y0gBauYfgD367tYBPJQjdftKb553/b7VXZi7daySyqkA/QXEE\nB3s18EThNsphsqwktloOk/BbRBkBLqJRQfpsYs1hqqC9T42X8rwQQ+B41sqc\nFhaqN4lNVAyB+dDPjtva87LBXvgRV/tElY1K5gyRIa8V+xap01XNu8iEdBPU\nqpMa5baL3Rrevti3XyWDiTODzGc1EKnCPF8kILUmV8rjKvZB2tfDwZSVK9mK\nS/5wEuvnnqS1WcPE7rr6W6Ce7+KOjYEqLHOlXnnpV8YWSEVYGGcGgZmWddHD\njvdGi+/Dx24RvxSBWJ0SjxMBmy08EjKV/ssdLI3OGquk6UwUtTfZU0KwEHmK\n/p6cDIychhE7Yj9gJSxMeLK3klwhxMlOhhRWP6pc1iGB8uMVJAl05Zdyl0sC\nIfn5oO3pLP8yJx3O7moVtVIA7fwEg6P629g5sygs+//3wzJtFgFSU4LZXvA9\nIsa8uFHecWoyKhuPOFe4qC9q/rj3hoeBJqcWPmkdwedJqy8cd49dM/lWec16\n5B3S\r\n=thmw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAhbRx7IngEkpt2lcTg8cHI8ShB7AlfcU2Skb+BLv5YxAiEAqZpw2iLSKsshIP2TVT91JHOLCLLetYnAvqVPcKXq5fw="}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.0.0_1599100082201_0.3699610753625675"},"_hasShrinkwrap":false},"1.1.0":{"name":"dynamo-arc","version":"1.1.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"https://github.nike.com/sprockets/node-dynamo.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"484e61e11b3f8127c7d7cc9480588dd682b2bdf9","_id":"dynamo-arc@1.1.0","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-j6aHSdpRNxJSTqE0J+SC24/9gNydnPDHj2f5gCCHqejhNQMUuRp4fvZWkUa/spLzFTzEfgnO+qWTeRVN3pTXpw==","shasum":"8c3300602c2ec5cf1fd658e5f5ea85cf2d171b95","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.1.0.tgz","fileCount":8,"unpackedSize":28875,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfWQh0CRA9TVsSAnZWagAAKAUQAIj3ylmfpDXzza+bbK3N\n6yTlYAHsCUJ/5Y9f2lfBNWs//V3mTJHNSJGMMYePS8gE78iZyeHeHgd7ZhyD\nhB9VvsYqkKI/qTVgWFnPhy86xiR4QA29C0wDyjiZYfesdoVaY6GFy/nU9QoO\nR1y68LtDDUbeWWbfDu4oGFP2/+PvKA+AyOen/1tdPBOtKwf3ie+P2pdsgary\n+HUKOoHKdjF7CdVZdk0HCXDQrFTwWaqIDvecCRH+LQoOo0piT8zF2BT7OAox\nOgI/2zQ++t0Hcvw4fUZyxcvdVj17ta/SoM0Pws6yygO2bIAOSGJokU/NzSZl\nRmsBgLsxqPcecoVTf1TzXITDeLRKNiFj56u+PJyJr+0LLVz/IO6Ic8v4JWCp\nhySZJf9Fg/InEhbwwWwp2eQxGy40s2R0Ew3UEhcsmEKg1wd5Zrk9u5RSI0gX\n6vn3sDSRZGe5+SnEfHRIaUVWzV9AMNlIMvozMoyVVAhnDmHl8s7Oj5kAQKsQ\nQNYA53bmyXk6pLf5TzizDmXT+w0m9cRzgXB8z/lVhYJZxt5yFYWTLl0smRUE\n8iAs6QRIA1VgwkUs5QqH1XT5LXLuGgJ9l9f4YQpipIlyiaYaAaxKonGYA+3K\nUsRXpudyZTWVKJiFHEVZJh2Qvf3nUjDwEy6n5uut9ZpA7GDDPCr3VMRQHdfe\nmC/r\r\n=4tsq\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDITBwA24oFUAT0is01ZVl4mxgPBjT99v98dkFY3KpObgIhAPcWn6JXWLfaUVzlTbl3UUBgkyvpzGZVX2JtwXMtvFNS"}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.1.0_1599670387818_0.30488016737520596"},"_hasShrinkwrap":false},"1.2.0":{"name":"dynamo-arc","version":"1.2.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"https://github.nike.com/sprockets/node-dynamo.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"623380ed96d788b70d0392d788c89413c17c13f8","_id":"dynamo-arc@1.2.0","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-uAP4kTcMoXrOsMs2Y7xQSGnQuRTEGPIKZXiU7zfXb8zCG3uBEOtlx4kB209Mq+OZhiDg7deBX3XzelRx9IE9eQ==","shasum":"ded93f5e110d65dbb31f9d1ad0e6c66190f481fd","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.2.0.tgz","fileCount":8,"unpackedSize":29001,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfWTL7CRA9TVsSAnZWagAArvoP/RZjRFE8dW6oMZAqQIEO\nyj0bTSXQxziMFwbgO6hHzmVMjTvYXuBStfAbb1hZRbzxKarkexRq42Jn/gOK\n1gOs/bdES/bZtJvLHKxk0agrPYRZFRZq9ZNVyITXHiSLI1Wjvw+ysgrF/rTm\n39kou38h1z3lqXceY+c98JSqucdM7cmTataIyUmQt2ZBC9A/9fxthLyZWAcj\ngZZG8uskNSX++YgYV7hBIUnFL3l4b3mZgp84EBbAbg10d/lwNP2OOdKzX59H\n6nRu8wKEMgmpOYWXrVyeiqCYBCB3wucVdBThgzIqRuxqr9MXXU7ViprwZEqo\n2jDqvX33KehyTPRKTf4H2RMxJuMjjBGP98xyA7C7mr8dEKkwFAurp5QBF9s7\nU5qJ+6L7Y1G7LIxS3WdD/kMdHnlbuezGWPyF4um+zdH8qonDW9PseOOwqgd9\nDFGJS7WQRnewVUVc2WA6s99Sq6/j+BzD2m4Qu/2+jIbeuYXThieJJOp38jLt\nHfVkGhiqludYJBi3Dq5ZweyVbZoqwFE/MjTG5ucCp8QiZWeBfzI5TBU9cSO0\nSAA6XdYO1SyYGOW9bMmdrSbjW313BzvsVucAMPd6AEcTDcyDmrlGizcPpvJ2\nrxMc/Nf0GN+pRfGKU91ZLUd050dMwfA6kAGV7X/rDIL7l+P7F0/xvx7sef1R\n2v4J\r\n=K4Dw\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAf33Z3DIR/Twvf0DYAro+QTZpSUhyuFr39u8YQ4dwKWAiEA7Np1i7hnr8U947pMGHUnO7u3F+NpGy3ii07faygxyNs="}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.2.0_1599681275029_0.5577691205016382"},"_hasShrinkwrap":false},"1.3.0":{"name":"dynamo-arc","version":"1.3.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"3173754c6322a31df96bfa9a2e16ca9a6f0a2337","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.3.0","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-uoUnBLC2IuI3ayyRQ1uoD3o1VSS1b2eoqG4GQplEB8qts/6v0PVz5la05vm4aVzoRvauBcdU2Oik2XI7tFXcSg==","shasum":"7efeeb0eec3a52ab500c9bb0c105d84361e9284d","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.3.0.tgz","fileCount":8,"unpackedSize":34936,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfWVSUCRA9TVsSAnZWagAAuhIP/0C6flaGuXhKN1x4DPbi\nKmtRWfQFkomdajv3kbL1U8Eii7/nxqa9kCAPKVx8YiT78gyjBAMsnMnaXGsL\ntzeBe8kbX0aOqV3haGZwjn1SGzkIjRlBI6DaVqy1iUozl8aSbrHflvUnQbzj\nEuM0TCDt/c5Byeg9TMmSTpz4ZySgY1i2QbKw7V1K57y/8xjCmDOkZQLzI6z1\nLrqaXfBjWsvc1AMlDAqT4wf/k9WFOCUFyOB9GmWZFRRZ5s5hERfwxZzMFoNX\n9+HIz6Mai2RVkh0x5BZjqNKFMrClwLqWdQFte5Dg3nZncZ3VwAC/+2mCvc41\nDsXXruHq1fWDTUQNNESOcQHGI79dDkS+rOC50Oddg93UKXjkNoyQJ/qCRoc0\nce2St+U5kWWzujALR1pUvRXFdO7KACMp9mZUgHd8O9M2cfqm6UV591cim4Fb\nchiOTAQkVkc5uw3/TduiRG0c1G6tN85ZGGd0K9o+g7hjJNrX0Hlh5CpnM3Tn\nn2+jhQuiVwq/S861k5jXT2PQO1kBrWgRZkytvma3ccNp/SyOui61df2ZJYMi\nOxowgBom1iImE4O7wGXTSutL7xashboLPmrUKJHFdGAM3j5xZ8RUr/F266Gh\nnXTr0UGmVmhSYXLWgIt+DOmUqxPh9RRCIIMugmR64h5gjlUxwwB86vRwYG2l\n5Ilj\r\n=uq2X\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCJCYdHLehYpYCT44pgnZgcV5XBQlrivEOSKjm2El7n5AIgK9CgZ5ElwqIqWIuBP3JIh69C4n4OR4B/s8ZlPgzt0Zc="}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.3.0_1599689876356_0.07313852621754746"},"_hasShrinkwrap":false},"1.3.1":{"name":"dynamo-arc","version":"1.3.1","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"a0f1b14ce2272e6ca05721aa6e4d95a7016ae21e","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.3.1","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-e4PdhelVgcc3saztWmNF0Z/gG3qBuhmp2eByL6z8wiMol4Rs8rUQrUlelf20yKV6crjq4IlFraLjI25YyO2lAQ==","shasum":"97bbf64bf28c67f6ae7cb0116a65bfbfcb5925bd","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.3.1.tgz","fileCount":8,"unpackedSize":35400,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfWVtnCRA9TVsSAnZWagAAkPYP+gMu9Ljqbm/joQjaTOy2\nwal3qMkK0V9SFMpyM2THrcOYFKla8C+VF7egv1pB9E/fKVqNiQwseioY+peX\nKTI+qwCpIjTCHkleefFL6AlxwmBlSNGsl4Ms09qcO+5FmfxDMd9r54hknPEG\nhMI0Xs7Wl2jOn+OfCT8gX90D2pnspz5Mkff9Mt3iiRoZxyAkP5OpYxreTg7H\n02sjJOfRSbpTe7/wqtOWGVMwa9jkX7WxcsxEWPpXhW9vJC1rwx/5yoPGcnDm\ncJNRKqvmQFfNwCID5GqDu8ziJc5Rrtwhh0m2o31s4AqU2Giyv28N+AZZq0Dx\nTrj13DCCWWGXvvBz3r0fSNC8kk8B9++gWSBtrQILbH3+YJHCZXxZlB1jqY3h\nVJ9uZscnPewzGaQE5Eda+XwoZafpTUG2ay22ALrEiPCPsqyWUHVaLFGlArT8\nmpP9d9Wu1AeJfb1N+bA7E39p+q7hXOVP6g4L80XCg3lwz7AGU/gANK4MR+Nd\nna642olI3QhkohYmUYn/tsfvB4eWzVoWStq+lgZduIB816lO0dP1PgjG4qBY\n4TkRNEOjk5PyTc3AZRuhyi2wo6LCOMc4D9oddmXt4PNlRgl2CjH8uxGg9lx5\nuFh04Z9qvmpZo9YkIcI/eirM60fsKnoTnaOv1i0wHWHP3scxbJoQMsDDrZ7l\njPkr\r\n=zNH0\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGFPFqDLqaKPjeUMq4efFIRYQT9HS4HCXHNz4o2mr62dAiBuZ8047f3d0Iu/yA1gEg0y31LOG5P/zSpabVjZSjwUwQ=="}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.3.1_1599691622746_0.9037208361380469"},"_hasShrinkwrap":false},"1.3.2":{"name":"dynamo-arc","version":"1.3.2","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"@nike/logger-wrapper":"^0.3.0","dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"e64a521f96dd7769fb382dd43ab7b40792df97be","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.3.2","_nodeVersion":"12.18.3","_npmVersion":"6.14.6","dist":{"integrity":"sha512-jrm5clqOFDMwli14ORh4ufEu11JFciLcOkjQYHFSu+yPBZa+znGsw64J8UP7K7QI92KOtjO1rFxRBLBsijb2Qw==","shasum":"e7cbd518ec68b29dbef9a82e8e42615d81ca9f59","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.3.2.tgz","fileCount":8,"unpackedSize":42746,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfmymICRA9TVsSAnZWagAAak8P/3D3GB4BInyuB28wJpjz\nP2kzyPwMfkPUtEnnAEDMUe4S0Ggn+3b//eC8aAws5A2LSLadNukQBGHIkt9u\nii3xbwPYKkikMhgnqyNo/IZOpeF0fgznEmH5cjCwF+xdcRWdB25bVn0NHlIv\nBy/OOSJXDdEp9z6o2RLkEHF4Qq1dFQWfgUYPcRflxoVdaAjSErii1gQJHZ+L\nP+6wJOsBuANXqRbEJGqBHd8BA055VRv3LuS7ezgjiL+2aT/3qcO2zKKTJIX3\nAKdnHNL3BZbY+OPvB3yz9bd6Jcijs5YBxYqcG3xc7x2GeDgBJHxDm1emYU0B\nT1/P7xGfctF+K0nUbe/YdzGNit8RuCcOds/iFIVeYZBajVH5Djm6nhfomIdV\nzukUY0A91XP5wP9R+1rkJvFmwm9NDalZOoOiEY2IUIylBDkkgPViKM4CZ82R\nO9USexNgWkZm7WlWDN0efrci/3yJnguyJ9AAqnw6lfml7UsOqjGLlf3kvjPg\nAlt5e5ZhYI97BXQ+xex/LHyrjRvvEGuAUtl0aS6siwRuIx6KEDk1iNJzlvFv\nzFefgoBG8WhxLdbJDw3z7SFCFrHNI/ovXGockizXGjaWdKyNufvom+lR9h9j\ncKwKMy6sOz2KodXoSv+J0Ik1yyeCizBayPSDiX88rB+Jh18YHh46gDgr+Y4Y\nz/4B\r\n=FHMW\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDtZxqBCsLVfLrpDMDDDoRuuc/EcKlc7XmgJ9lyS3+VFwIhAJcMkhbitDwX/aUiyfwkrYHiq/b0X1K48teSgfF9RbFW"}]},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.3.2_1604004231475_0.8981923568159562"},"_hasShrinkwrap":false},"1.3.3":{"name":"dynamo-arc","version":"1.3.3","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"2a4e258f9c5effe96769b3ff026b59a06f45005a","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.3.3","_nodeVersion":"12.16.2","_npmVersion":"6.14.4","dist":{"integrity":"sha512-yvpFV6IpnDlppVoRnilO44AcM8Mj+M5ZlZaXTEHAeNQQh3gYyIxlR08TpWg7P+InngMF0MHnVOtl18qimDHyHg==","shasum":"1d7839ae8066b0edd44bd5e1a6e49cca90d431cd","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.3.3.tgz","fileCount":8,"unpackedSize":42708,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJf+2g6CRA9TVsSAnZWagAArqQP/inRl6SDCF96RuLrCFNY\nKrmQcOkBiM8omm5PNCSYirueWzAMG1q93NvYkjO6r3M6W3vYjq1ELF2ZNZpP\nkrIb9Txu1+UNo2Kt+s0pmET1190exkdVMOH9sbcc8ErnCRfzj3UmlWxrLjM+\nv+KkrVucwdrSd8ATIydLwrS0dKuGB0HeN3BkdtfQq8TsYdfmemUSdTwtEK+5\naKMAsCRDxZBaIrPWf+AtcRfcF8STo65v82i+mk+eYncyt7Wwzm9X03z2Qbci\nBcn6oOFc4Pl/fx/wx3rKDCktpeWKj1HlrGSh9hLPQ/7C9F36bfyAk8qpb+Y8\nqmm0eXNhcRH7eg4S9ExyX0Lumvq4oloJQX+KQSjmdLDmpMuYRWwpsV7LKdob\nOEVcT2FRP+EPz3l/empsJosDU6MYYNm1wAv+3kpFQOnZNS4s40RwBuFI+NcB\ngMveC8rHKD3kNVuElEJfhCM3cHuyaFgYjM0hiet9RMIVGSLNcuis60m6RYcE\ndlH8t1XSlCBdX59UNZX3Sk3ZkOtuwCQziTKYTIUuLOqxF7zeHhefmkbcJPHO\nby0LPhv+r2GbtpjkOqOTaK+Z5VgGpMHfSXGn2nnmRVU7GAxl2XpsdNlbEEQl\nox0PeNcaXmgPukwvWuHKLBylEFKRJ6pGBWVlCONkmnB2mWzcH4oIrltxG4kB\nRDG+\r\n=X+lm\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCfR8ecRB7UtHanzbOBrWnZwxU2UrlxrHx/a2WXMJ7+RgIgDjXfHk67HQaYFOYQOG8Lkw1x5BTmwi/pVoiAPMhVgCI="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.3.3_1610311737509_0.08438447532143023"},"_hasShrinkwrap":false},"1.4.0":{"name":"dynamo-arc","version":"1.4.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"636c924f0995be593e6b8869ce0a75020af0c7c9","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.4.0","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-kO55U1zr7Rq0vRkhk/IFYeMIIejgZC4lPFwwRwFGksDRIadNb8+XXiGXHs/zzZkEtG7M9ZRje0w2OFn6f+ChTw==","shasum":"f4312fe2102d5daebc059841e3945f545cff1328","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.4.0.tgz","fileCount":8,"unpackedSize":43998,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgCb0ZCRA9TVsSAnZWagAAQMIP/iU0oMKi9co5FitJlA3Y\nQvnSe/mkbyGpVVvjCoH9ouzQxJmouGSm1RWj9P8SuT6ihlpTO+cR8wVYM7Qc\nawJBXBMKfYEMspBlXnMPHIOGQyLm9QVPCcogxTW5JrKUgtcqcHzclscQZDtS\niYIzZjAikZZhp39yLSNEJOSDjo2HpV1DlsxNB/2qTz+y8JIC0SnFjnpeSkx3\nqL0jUilwq2sXsAdrLmGhzHROh/82wim6C0l1NLzGMyFEJE8hop8tP9npVVJQ\nJTBn44CACeHSYOsvhKsjvzgmVD6734U2dD6nhE6uwgC70jSjTSmJ/Giop7YG\nwWQH3L7eheip5fGqgTCnl1EqsLqAOGh3h136rK+Mnm4846L7+0+j1docZI/R\neCvvjidTSZmr6TXE0PLEwEONXxNnh1EHNBCidu3uZH73ys/KhFA0QRpFjzfQ\n2XkI9UpopU96ADgxcjb6CvLY/w9c11Q1icW9kOc498/bA9Lfu1CiEeUCUg8b\nuTKc65ZVM1tkFEXLl9dCJSGpK0ciEGsC5sx0c/XZc6TYJkzfwwka9COycUh/\ngLfaRfwmstrcCQHx6veOYrtuTRQDaxvj4Xl+PQYFDOr0JVWFeGFyuxkJPDHv\n1wqma6L7iFwlaWW0RYEAF8sAdEdxqobtWNwndlsTFuQabi+iCDOaokOn7P88\nqpAq\r\n=4x2I\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIC5MmaTHsus7oQfZIiY0Bsu4JgBuoGDXv/O0hG1A9OcCAiBmGLxRmBkcK5aPm6g5+fuc91xey+E9jD15cbv6MGSjmw=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.4.0_1611250968793_0.5399075021318822"},"_hasShrinkwrap":false},"1.5.0":{"name":"dynamo-arc","version":"1.5.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"b1046604873280782e06688d356ed9e6698289f3","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.5.0","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-5WnVbzcFnEfwdAvcnSGl5GT43HyS1HXwU83qB04WO2JSs1BvXiGjxIx0jDmYDc9rufyb2hAOXqthDCj0RCoSHw==","shasum":"f5c0d9fe6ddc61d104d5950694bef04122b3dc95","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.5.0.tgz","fileCount":8,"unpackedSize":44476,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgCcgbCRA9TVsSAnZWagAAC9kP/j1kzFB8HWKJOFjw74H9\nu/YpoA8NIyVbKE5oY2bMcnFmRhSU33Q6mCmpcmGW7tDgb6apIkM7Osf8SSLH\n/TyD7XYyQIvNhHRDerS9iY7HmlSyafIvvaHUwlWWMz/HEHUYFXU087nd99G6\njtbXIyHFKQ5y8lhN6oOnN934ecPc/g9Lp7p/ZKIErEZ6pxG95jyfIIBnMjEs\nUABzH6+9plxsDDvhQ4xPGm8P+BpAeX2wZZxTqkPAikbRP2NIPqRb2bILc8uH\nXBFAvDuzSoXZ24/0D6CXYUq7+TzGlCDMM27wY08tyl2Zsbs/LrZQXW1O2PWg\nTtBBgESOImlLvkb7zJpr307QK7se9NJk9BwPQfeJAce7b0M4nqIdeQPiBJL/\n1QKG841lh2CwHncxI+N7z3ZxJhsBBiiIm3D5+XYMVre8uQd+dPr9oW8tVwSb\nck6A88JQTJvpksL9lMFoNeeRuXFJlwg5e2LTU6Gqrrhf754PLUI1DP/asRwB\nnqWE8apbBrcRhrPfT4eMx5OtXPI6F6kiBNq4qIQdEDvDSgfAqSEBSv08z8lj\n+rxfrEmDyEi3y5VoRLAiFGKoIHKOlKCWQ4uuYAczIdgPAiJSjz8ZK5tJ+WiQ\nIzLj+ggOmyCzxf/kw2/mmswouyMDUlKorpCdPnUoLpyACodk0u+eUVxbXcUZ\nwR69\r\n=tUyz\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAeVJZ4LF3kuo4Jk1QItgGaGIrvV13kDiBkHWJ/MAu0KAiAzXJwLZ+pMJQ6nMbifmu8OPsY5oxF1laB4oYQCVNsvdA=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.5.0_1611253787206_0.07471342615010945"},"_hasShrinkwrap":false},"1.5.1":{"name":"dynamo-arc","version":"1.5.1","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"56d4ab365c4e8dcffe1dab6d390cde4b8c45c2eb","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.5.1","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-spslQ0iPYJnSpdVZFbv2Les7Z5jW+tFbDeqWfO5h8aIBrwLSoJQWCd0U4+x6H+Jsh5b9yAcui0knDb237TOgUA==","shasum":"6c656a448d809e74d54ccf539664d7b9442dd7e8","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.5.1.tgz","fileCount":8,"unpackedSize":44585,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLAYoCRA9TVsSAnZWagAAo5QP/23voUKmHl5KHOb+N3QM\n9jvyg4KovbRlZEzeinmgyPH/977LxdC4x20+vzI4U5gWCeOMsqC9KHCCD0TF\n2xzwTtUIQicaHbCBjvniWY8OTukenyP1Z7CkUS4UgiP3KNL9r7MPjUo06x9T\nJZ20YtEphjHIjmpM9AYo+SDCVeKwVHjlRYTSMzyyfxOM+s2XFKIOvZf3R3kN\njuhOo0oCyAftia/WE5kfyUmSZEbBT0HopeTkA0qtiEWQe2kNBMKHuDAJdnCV\n9yHjSmZwxkk7CJtYxKTkaTU3OBovJ+7NKh1PxLL1ZdAx4NIeg8d3G1KXtGcR\nWnd5V0UsPiJSzUda4+i869Lm2X52LRXViHvuuKgPPyN2bCB1momFCZjBuY46\nBJ9CTZfUmx+YoAIB+mgwkT2pRBO5gf+e+qgQ8gtwGe3R7XfyK8m3QNU+LMtI\nR1nN+XoiF/osALHPmo1t7m45UxwB1iTW6sYQZWlh+7ur3FqSo4Z3cbubvLrU\n+qGl2YQKheOWeGg0A5Xeb0Alj0oW9gbBsBbBPKSTuNf4HCILiu9OuA+fR0Dn\ndwj73UNbGWefaJeuGq2LMlHlgsJNpUPVnXwZQeQmj+lo8zpzf/vBfLYYRR+H\nCTYBZ3d5Ot3HV+ujuvbwk/cOfG9YweyrwF/rkRzV8vouaLn8OnH/KyZtErd9\nBRTv\r\n=TovD\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCSAoHPHWOvn2ii/N8qj+EpgUaEOGSk/kTaxxSv68ScWgIgUd0pM3UA7U6xsJz/8POrQa5yiQFvWm123WNkTbCSN54="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.5.1_1613497895917_0.9426913658498761"},"_hasShrinkwrap":false},"2.0.0-0":{"name":"dynamo-arc","version":"2.0.0-0","description":"dynamo data client with async-friendly API","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"dependencies":{"dynamo-butter":"^2.0.0-0"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('@nike/dynamo-client')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: DynamoClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"1753d76bdf75495411dc6e33c5dd2668b171bbb1","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-0","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-Q/hMCNZrNoR9QG2QYgD++l/REbdmciLvyilQQuV1otCiAho+h946T5Zinh4L3ReZpRwFfkq+aRmwPMtDaUs+Tw==","shasum":"2cc1634cd93bddcb208520807e1a0ff7b1fe7a0d","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-0.tgz","fileCount":9,"unpackedSize":56401,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLZdtCRA9TVsSAnZWagAAMHAP/Avb4EHXKhP11wmJJBvg\nroe2RG//vNhA1KsLKJBKbEV8KHb82GVaqL6eDBcbKUJy+OWuoaCtGa0mxCnN\ni8fdV//8oFzCrr/GZZYUPR8advKVKDbDMMxS4g1yxdcsT+H1fMvCif/VDEqv\nFlSEDKSaOVzSBy2GV0lUHoUrb9WhGb9dmV03jE8BOXCDuNijogHHymrfhzps\ns7MD6sk7QsrcRrF2ybhPlIzjcbfebn0Y/MI8GhuA7yjdwZ8gsIYCmr9PAuZ9\nDXCFdf1NTVLrQ6aXLHWP+446Djyfm9F2zKE38Meem6bZ4W2hg+9PG0ubFP9S\n+rZcuCpwcBQTFJyihWA0pJ5Htb70m4ja45iXx788BynMQ1TviZ62LH8FbrTd\nI1hoZuLewl2/JH0g4uFGj01FnRAOMbx5JqPQEGQiYJwvtSh/Y7m+UjKU7QUa\n6CamVpQSM3WNgDXXBA5vVR9ZjUkZFb0OShi3w1QCCCsL52coUTAx+s7//P2y\nGlAgR0szNFfd5xoivl7Wb0FizPv/v/EyHFCqFIlrNIS00lbOBZC6HyPvjm5+\nDb8u9cDw6np/t+rbeMqGgENbwYSAHS8K34raKOMknxx4LhHqpXGA+tpPYvus\nNLWDDatlTN+eqKG4TbQAcm6C44ZutzDRCz3pPasvGIjlhhhG5MCSH2VXlDiZ\nq8jm\r\n=e4Zh\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDrma5chzSOhfLoZU+VQ1BRPPuzzJlnLlrcJ16jsmqntwIgM4p8gk1qxJcthm6fT/uXWQ0o2R4bumnJgdhDJq6/M2c="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-0_1613600620569_0.056154011992723296"},"_hasShrinkwrap":false},"2.0.0-1":{"name":"dynamo-arc","version":"2.0.0-1","description":"dynamo data client with async-friendly API","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('@nike/dynamo-client')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: DynamoClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"7566b0da44338055c7885abd5748c8cba5d7b7fb","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-1","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-8pBWOj/JBd+b9VogD/teA3j8dbIczPPmW52mXkK7inzGSPjcX80A6TLWhK1K1QZeYgFPPqlBNkKh/EMTmiIp0g==","shasum":"11e338207128bc825d9f7c7aaa5b3292472d633c","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-1.tgz","fileCount":9,"unpackedSize":413248,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLbKbCRA9TVsSAnZWagAAn08P/RWmEecmsVF586NspcfZ\nSEOgQB8YMWXoOsrIqqqIBHfRjJT2dMd5/snNpFfygKtQB/mLRWMBnCPA/TrY\nVQLAvfvEbaZb3qLvpIPhFQkVX23hTmtjnVI0Or1xWnUc5fuUeDuVsZluDgHO\nCmUN76YEeqanHwFlDKCPTSrAqW+2YxCaoRH2d+qOiltqT1NGLkt0+voFkviq\nFivYNwI5Zi672rLJ5rtn09HjO/hH8CO0glq16EoEgS/l+5dTFTQw0BR+KgYo\nO7hBfFfOMgHgitIoo7IKPRlLhmCctcZ+T/QvrlUfjqPef1m7TPUpGtZ8ca/8\nRbQA6VHnPQxCxanh5wshY/gFI/Mla1zh+JA5cL1IXDpK/q1sJPchi6jXpKvj\ndd8IqvZw100gsV2G2KyFtPp9pNwaSiKJ7Ymt2CVv3UvpdQ3BVR/qMUGSsSlo\n3A+Yv+0kLR9dfMQg8L3mrylc93L4gb6Ucwa6BJSlyq43vQuuDuJ332oR13uz\nZcO3mYFNxUmMw13XVR7s0jHdKignb/fsI5CzUW+0AQDaNnxuvClsSbEWElCa\nV3jp464skoZpm+2Jk4QnU52OaViZrwvNG9oZ3n0KMkCKl7YPVtKYBVtUvIRA\nWNr76swhbtUdLT02j3ZbLWg5PgNrfGbjZmHWnh+s+MP1XaYS66Wo8+ESxria\n/CC3\r\n=XKib\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDTagDhfCjgTv9KvlF4mB7dVyUf4cmjDDwGOGQfKmiv+AiB93hsHf6dtkmw/SB/oXMRWyy6e/feLmoCdPT2PJiKUcg=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-1_1613607579404_0.5606347546304837"},"_hasShrinkwrap":false},"2.0.0-2":{"name":"dynamo-arc","version":"2.0.0-2","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"507bd9df95c9d59c20f5fb1a896cba109c9013e9","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-2","_nodeVersion":"14.15.4","_npmVersion":"6.14.10","dist":{"integrity":"sha512-cz5J5MTDuCEd5FH8FMP3U7jECaXQXodAFoz+ii2RMe/hcJfFbP/xYxW5FEBX9L9OvGmPJa8EFgNjSNRKwehOqQ==","shasum":"06c6212b5c2a3cc8162953938e76a3e5fa2d77a6","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-2.tgz","fileCount":9,"unpackedSize":413237,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLgbICRA9TVsSAnZWagAAPDUQAJJf7dt40WGAHWywP9oI\nVrF93nOSRzDbXx0fdV7FHSytyKSDPJKSzaWz3pdNk1GUhG0l6HgzpyTkO3W3\nCVJFuCwb054C2swlqNGhb0A/tWn3HkAwgy0J+fGIMZBDKDzqTt1eF1luZTJx\nyh6Gyz7paO8pRuhqjSAhB4kq0yElSiPB0aqGt8xSu1r9dvkJAQHSQfG+TX+v\nR3WhvMiul3ukpzfUjMBl1w/jq3DveaE0CpvPRSM+erDHOv0mUtsYIYCsnvsg\nm1hnboil2E809/1LslLU3aq54vXMi6tmPcS+PI713dXl/z/sU+ryPLmC4V0g\nU7dF9AN7IxcQfNcgYAslxNCZedZyF1SzIV94sTnkBRGYKIVMbZLPuSMgk8L4\nWN9lWamJWOFx0X3SGuKBePNe0/AhMtk/FaR9S41mGlbRMCODsyLH3GXow/d2\njA9tRk9d7wL6MnGb5k47hjxlnKSW1aQApkXN0vvUELYvEXBh5fJf2ldsIRmW\nqRboFm9w1Ip89gIhdTw+2tc86zkplFfA/nPCOZGxrHC0bqGqXx4SasMPEhiF\nGFtFTVF8K/7k4XjuGp9SE9KLH043etT7tQhXNdoT6tJF5M02Ud55GcLptdeV\ncaKQIAqH29YP3/ieAYw/KZ3WIF8zWi1CK4SFXeW2DK/LlJt7oL1fpEEXiiIN\npkKV\r\n=Gm5/\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIBso9QpknVHWvHAOFRHa8jbzkq/2L8xDgA5WN3bNVHVTAiA4cdJLvfZoykYJ00zTOHiF87NCYrfGT6IeEFs5nYpZJw=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-2_1613629128316_0.10866956332634747"},"_hasShrinkwrap":false},"2.0.0-3":{"name":"dynamo-arc","version":"2.0.0-3","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"d892efa33ff4941dc76b54dc781d61f380e33084","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-3","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-ABy2YXwxmeoOKVXBtGH642eyoZq4vyxXt2Y/zeFf5QgETTq7kTUcSPfKQsZST4PCnw8bGGIGe+zo6RPOLV11yg==","shasum":"651eb8448bbed677e4de90933fc53f7fe0da5918","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-3.tgz","fileCount":9,"unpackedSize":413446,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLuBGCRA9TVsSAnZWagAA11oP/2BxwliK5RpA8/eQKfDC\nmTXNjJn7j84gA06oof6Q3M17mKVvVR6ZG7a2vf9lPZeCc7015KQpOMBL5bH5\nNWYzrR6w2vQy/Leyl/NN5TMAfhDu2JA2I+ial+JRGQdn22NWz4Fg+A1m7IHH\nZ+Q5+sIv/XrXo6TLIjxKSPpG6mwhHBukOg2Bj44Kun/ElSo+W1lRY8FyHMhw\n+f0BieUb2aybpfjG2bIjl7ldr27RO+shD6GFwABMsyjJBeiBEUFJYjHLGn4J\nVjEJiswSC/L99BY/HeY6sU0HsJZrNCk5K6Un0MkKSDaxyNVkBpBv5cPbCMnf\n3Y61a2+QIXBGafzMjt8JjAFX+YrgsczRTCE6vehG+gmLYUeop4aVGuG9O8EQ\nG31JLJfxr0mzb/tO7816ul/AVZudMEgv3CzgPdagiz37dygQuJCTcslMhWSw\nK/Fgw0LkgzLsMm4SIKgn6A5xwHqshUWXqse9LiUTBYRIdznFnIkHDERGvgHI\nvMcv3AgAmVymEe2o+3EwJyeHm81oiCTMGkRJzXnIDCaIs7+jOj/e2Uz3PXCJ\nwMx34VG96KnruHoFDUMEmuNp08C6+gaV+WBm9pBpuzP+n84+9OAfzbZLfyR2\nn7voHSx7myG5Evb1hy95OQoURr4urJ07+l3CmdhmVWlrBXmPf/65K6mQ4V3k\neMaW\r\n=USdj\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAn8jk8Z1hbZ39HahI/MBWZszI+wYe6AFFbX7ude9lnLAiBxOwVVrbAqATslygVYokQjrQRA/ih9QxtRqeaW1ZSzKg=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-3_1613684805395_0.24353494262658515"},"_hasShrinkwrap":false},"2.0.0-4":{"name":"dynamo-arc","version":"2.0.0-4","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-1","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"da07bb6d6b0f697a07940a9169bcfae9a1519870","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-4","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-n2JTjZoeHmfU6H2T2w5efkQPePkL4MJlw3gQRDwd3h1uCnqJTsfGVUA5WBHiMF1R7R03qk6DxNo1TKRCSxblqw==","shasum":"9a4a8d89608fd6e9d2c3c181f3007c8f32a43705","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-4.tgz","fileCount":9,"unpackedSize":415357,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLwIGCRA9TVsSAnZWagAATJwP/jc5pE/0kQMe3wwiMQNA\nc9VlZHTYd1fMQKtjCiNk7NjKtqEHu9QGxm5JkFytbhVRLF68PsvodhUqnUsD\nnwPRBp/HX86LJ1EclZtpnT0O2Xi295IACaT7v/lv/zTVOBLMN2g4hIIqhDrl\nSTD6nAuLoJkWwwtzG0th31eotSQMrmYrvUk1q4jj11hQirGU6pdLtoSbwQhp\neCXNJo958x8U5bcMUIXsXi4qBqjfpU0ZH/PbNQNFMwjM0YOhCQAVFir6UjPv\nu8T3fVhiy6yXqgc61QNtcyzXzsO1wOBhvBZ1C4Ex/nkux776RIvCmIKdtnVu\ncMAAbOKHfQ7u6AJ2XuCb58C5K4UJLR6b9hduYmG8jmmVyKi4EhhoiiQSOnBP\nNTcrt0K/C2ARP9uvf/DtaO6HuF4J4VRfdWrQZ8pxl1nwxgGN1fV+4HcwN2gK\n8JN8y++ymP3Oatxw3awHbntAvHkgv0Ltvrb/qLy/njgGImS301G4DPB84VLZ\ndtJesAM4Jos3x/fiSfqYcDlPlFFKQTMZQ+sbj3bPwcCYvrOySGtjOTYZHdFr\n4wTMeRveqfa4NV6IAJj6gNDuEP9319xGImIJnH4w52Kj0dNg7ed5VhRK3uYj\n3iXIV2liFCj5sqg4voNQvxi3UHtCys1lGtnMq4jrwvMEnotetziMvRe3uDmG\nEFCW\r\n=80wB\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAxpvBaiVjb0/9KLquG5F2y3uLcpjVlecp8YRk6pStAXAiEAtdiy1Zp0tcJvC0B4O54L0G/W+Tn1Q/g2ixXt4wSUhzg="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-4_1613693445753_0.10186937765851645"},"_hasShrinkwrap":false},"2.0.0-5":{"name":"dynamo-arc","version":"2.0.0-5","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-2","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"e6448769b78974bed1c0fe1c9511c9646942bfd3","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-5","_nodeVersion":"12.18.3","_npmVersion":"7.1.2","dist":{"integrity":"sha512-4qgZjtHKr8awTxgbfhqkThl4BdDuRcGuqAMPB1TpChhZ+A6R1dvKH0RZ+mVxTuSs1cIuyQqqZQ2U7Lr3ezx0eQ==","shasum":"62daeb74bdaea10c874d73541f327a3a786147c3","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-5.tgz","fileCount":9,"unpackedSize":415417,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLwtQCRA9TVsSAnZWagAAIFoP/jYx6R9tvOaEe13WrhZQ\nTGAm1oGoqp2RQshktHgW2A+Lsk4/KPKkzxHWLaaPzTlum90MEWItajz6ley3\nymL+oY3bSjfbMWlM+M6QrRcfTk4TsG0t6tKsnR1a1sKhXH+GCDx5V2Fgx0Jh\nbbsnaSTgO0ZT3zavbuqQo0doG7AqAKW3npUX4QgbvIriKsU4/ni6NqU/efmN\nXpE8CFADOGS6eoqfggrKc1hlI7FJjJtt7pnLujPjgD6bHQXVb2CgMrEKzDGi\nTdgC1fU1QSEjY3HFkrA8VciFFjRxtfJUdTZkTB9sGCmaTGCJuRYuiH5TL3hX\naISeOfZuShIdhaXrznzxDpfgB74/0ZUEupI+2ihEbTZDprNhk1s0xhGYP/K8\nzjnNuIDJcqo5yhvhtIZNy3BpJ7LvZ+xXwcMRrs2ttVqPhvcT11w1xqaEzRZX\ndc0CKAUKH8LfMTMwjz0ePP82X9OggV/8awpyuVi45WpCrMXLau7ZNwS+IjpY\nVv511GnNEHNDW5TjIO/nIIXoFar3P+yqFRW2DYkGq7LeuwWB8rgsf4frjDl2\ni3KkFv15HAdF9HyBSlpfKZDpz1jYw0C1+E8uNZQkC9yJyxditLEwE+RQGBP5\nxX0+CPsuSWGgxA7722r96re1gLIjztdHcmxx7GeUeb7VAuaxfa91OeBEFJzl\nkIfz\r\n=1f6y\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAj0NJOaFKTa2hn69+XPhBSaM6VRKa90+dKKFa4oHo6cAiALtWZ1ttClNXUM/h6UIn37QVFEhhh9XT8m42PPc2/B3w=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-5_1613695824488_0.3337029584857292"},"_hasShrinkwrap":false},"2.0.0-6":{"name":"dynamo-arc","version":"2.0.0-6","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-2","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"4eed1f5a6f532629aa26a4f46673653f09c705ca","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-6","_nodeVersion":"14.15.4","_npmVersion":"7.5.4","dist":{"integrity":"sha512-nisJ+3yxVsszdzEKHGkztNzVYte0hmlHQ/xisNieKnYXsam9nU4Bylk7/KEoiMnT5rS/z4oIUxW0IwmrQZFm5w==","shasum":"64c5a4ac1933a9d8acdcd614699205d9a7e61f82","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-6.tgz","fileCount":9,"unpackedSize":1044845,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgLz/ACRA9TVsSAnZWagAAVdwP/iNwAZqi2zWBkkvB8HfR\ni/OCDfFWec/huTYZAHYB/2okR+lhA2SYa01rkrC7CCBCN0RgUlmmlGxKxoEI\nt+ceeTitjJ2bWGCNsYhp38pfDfxF0rY485dmBHm/kk8V3uMtQE5DasPYfUxs\nLVwGf26hiZ+o2+hb6DJNvEGHGqJnI5ZQRiNMC6dU4XwYw7ZFP2aoMgKPTIHY\n0O9evNnWFgwvq8GsQUw69i/CUkG6Fe3hdJkb26W1mT/+4x5UR3NIxcU1mOsA\nGwKoqf28aj3AKveHHxyeekZ1cABnMw0fzT5bIZVTWcEeBDPjerE2//ufi6k8\n8+LAnbP3BYI1ey03J4wYV2LbzC0ebJ96HN4Cxr+Fpbge+eePyMiey9FONNrl\nnNKY+JwzaYXRJlMBkwja4HENEp/SBhaQWg3OfxdoML7hPjec4ai127LLy5aM\nk04YKWyiGwp7AYJHMTlbTohmsCGzB+kaoWf6+Vq36Y/9JxnACh+jaVXQn4PW\nJZOwSdfaH1G83Tqh5QRMBL8Rmg9PNROb5EDwugE9qzWQ/d5I6kCPxpmkN//q\njLBV8siRap/AFFAK8vMNz4CrjXj4TB1QysEKWHHJCTp4hWyiZoE2UlEymXFZ\nZaU1H0Gr0t18zvztKi+MrSacItr0BNs/K0qysnSEFFW//z8HZNqzVoSh3XXk\nDNKq\r\n=ZBsY\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDx6RauQ1jB9xhAPk1kzpag9XVJE5yrPELQkP0zRmKTwAIhAKFP9Ue4HHR2etdkJo0IF139NrodQcEw1JYHgDC6WYsy"}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-6_1613709247771_0.6809636951084086"},"_hasShrinkwrap":false},"2.0.0-7":{"name":"dynamo-arc","version":"2.0.0-7","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:types build:package","build:types":"tsc","build:rollup":"rollup -c","build:package":"node esbuild.js","build:clean":"rimraf lib","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-8","esbuild":"^0.8.49","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","ts-loader":"^8.0.17","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"6873b4ce90eb6b7ff2cee3b5a9786d1ba4f5def7","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-7","_nodeVersion":"14.15.4","_npmVersion":"7.5.4","dist":{"integrity":"sha512-LDgMzQYhyeBCzyX+lFMWp+iowehN6CTYTpfLMJQCv1ZvGW/08ErAi0QcDR9zmgX1kqoAqvptMUwlHKDsHIL0bw==","shasum":"dcdff15b78e7c07d34dc1e3185840605784ee3e8","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-7.tgz","fileCount":9,"unpackedSize":820755,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgMC3WCRA9TVsSAnZWagAACRwP/RYUYwEX3gC4rgTBpxLU\nB2puYXYC0rDNOjx9POPr4l03+n9RTz8xIeYo9oyjHBPJm0cIgQfRuup/7dgO\nIOrzFW3a0rdntWg7Py6L66pF2COcUwZqchz9LosHGeWAoIg12TCPDZWbiUrM\npCnXmGHZ9J1qSUq94p+U9oiKPQrcHt++mPk6+BbQQwOcSfNeaLogkb3DwDey\nrmvqZVfocefiB0i/1hU8PUA1CiKVkys5XvtOIeL6gDJKAoVorO0VSGkUypws\njKyrH8x2zaI1kSbLJye7Bnwrf36x+Hr300ZdL5/aPw9+zQLg00uZAwdxJtqc\n+Sj45B81qp3zncCF+0CbsHQD0XXdMRdlFOZuzHdiVWdXs5FZ9w/4ij/x6cIu\nu89Tj5tffaoVXeWxM5rsp82YH6BDKLJYN52JOBT9Jbfd999JciiCpqACuGkp\nLPhqNwZHNufgA/y+as/tM/8gtmZixZliTPyqRwSmQpkwpHkzCU6HWzsr00Xo\nh+HozxLzoEOlGXxT5LmM65E7m40YUHbICrpudgKJC/2kyNAtb7B789s2z0v9\n0re6F9WIBbY1spcLUsqBWHySimUKQNGf5TUiFcSOUG9MyAin8n+22fzjgqRg\nqqW/sCF9dsaaQ8Og9lZVKcSKOmgL93cSD4cjA0guPDUjVKLFQGXMN20JTs1z\nlMwd\r\n=9/SM\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIH+2IMFo0AvVSVOA6Ct+8A0rAqc9C57dEoagMKdFhHvjAiBXZ/j6GkrszTbQ889+i75IaJGS+cssK6eMd2hvCWZUAQ=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-7_1613770198414_0.23261761507708267"},"_hasShrinkwrap":false},"2.0.0-8":{"name":"dynamo-arc","version":"2.0.0-8","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:types build:package","build:types":"tsc","build:rollup":"rollup -c","build:package":"node esbuild.js","build:clean":"rimraf lib","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-9","esbuild":"^0.8.49","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","ts-loader":"^8.0.17","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"f0166635489447dac3abed087d301d8469885643","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-8","_nodeVersion":"14.15.4","_npmVersion":"7.5.4","dist":{"integrity":"sha512-S0CT2GkBO5wDiffAN/pcLdRc+QyrWk+jkTGAt30Dphc++0/UVvqee6/on8wIGShK5FmX0hX+eILpNMpNorwDRw==","shasum":"0335e5aa48c4a4cd36bb196fc3ed0cf42453d1bc","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-8.tgz","fileCount":9,"unpackedSize":820890,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgNCtrCRA9TVsSAnZWagAAKkoP/jYkW5ZPrurRspxjmzUO\neS+sSevLcJyMAmUotPBdbG2+ni3h2zZzWcP4lRSwJUtelTesg9mgFJ64agP0\nRrPf8vhbUXfN7TkzrmneuzzFQ/KzFO/lABa1Hkk5xDi394b8i/5BdIh7+cpz\nZg5hTLqoxhSkRsDLA3Dk7hufxa9IHK9Mu9wIvrYpdJuHOTz1XX6G9KU9ICpn\nApEcQE+TBUKjajOvE1g0Jz3UiEp6+BhiobLaKnpHiZW4qSdneCshFPKPxQbv\ntRnOfTiMawizDp79o19bMsKX4J/LMwJ5CA1WKpGKoRzoCj1jbxNBnPExDNrP\nMPsInZw67J10PG6SP/tlJgDRl4xdyXNnethWS93dqYR0JBl1tR6DxyDloMhh\nJgWbEiegkPEwZKP2SIK9s45yutLtNlMKV4vGYSBcaGobnIV9ZcHzL3OaaWPq\nmF/wShs2U7sXJ68wtwlmuWpLeAm2QplN572VRgv7GEk7KN5DwLHgGKk6lmYm\nziduiAecdw4sFOSQidXsaBxE8xsKt3OfgOtixvoDeLRLiOmkAWESDutMvjNK\ngbyfMbHYzqLLF3J0qwux0tJFyybJd/DmPs1eJANElitSJMd6JY6GSBhevioV\nuhx9ijEy7uukbnLM/pJRSnspG7El4IpvCxj4Pi5nh0DAdHE1RmYAbjGHtHxg\n+u2j\r\n=09ht\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDJIuY5gYU4fggpXiHJapZ+dPa+7Yk4nUoX860YxV9koAIhALMJepusPOBoYeAM2vo00TTT8AA46dmRe/U5HfoFDowS"}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-8_1614031722826_0.20365711388162944"},"_hasShrinkwrap":false},"2.0.0-9":{"name":"dynamo-arc","version":"2.0.0-9","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:types build:package","build:types":"tsc","build:rollup":"rollup -c","build:package":"node esbuild.js","build:clean":"rimraf lib","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","devDependencies":{"@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^17.1.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^11.2.0","@rollup/plugin-typescript":"^8.2.0","@types/jest":"^26.0.20","@typescript-eslint/eslint-plugin":"^4.15.1","@typescript-eslint/parser":"^4.15.1","dynamo-butter":"^2.0.0-10","esbuild":"^0.8.49","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.39.0","rollup-plugin-terser":"^7.0.2","sinon":"^9.0.2","ts-jest":"^26.5.1","ts-loader":"^8.0.17","tslib":"^1.13.0","typescript":"^4.1.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"2924f4898a788e49b47a58d858bf2b6f8473b584","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-9","_nodeVersion":"14.15.4","_npmVersion":"7.5.4","dist":{"integrity":"sha512-lUI5RnrfGeQddN7wa4qc3FiA1OEiAzgqUWNNatmkH5aeWQ+gC1ewH/KZ1YAX5G7PAT4n4fEgByfwSNb/WBCQDw==","shasum":"65877e8853d1bcf92c32cc2a2b0b33eaef821845","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-9.tgz","fileCount":9,"unpackedSize":820957,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgNbzDCRA9TVsSAnZWagAASYUP/2kYA8kSgEX2S7JNqWhq\ni/tOaToijh+3joB928ea3K6YoHBWZMF4BtM1Ailb6qKOMZ0tlMQjDSntnwoK\nKM+rMiZuIkpzK6iTmaNod7/c34dC7SrP/Hi40MgfvY9MV3aOSHFOdPDwj4it\nWNJCWCqTzAVAJ1DTYHxwLGdGKbuL6mn7LWtTbL1GFXPrQZlC1C3SmkUmhF8e\n0k1pCrM+bglRToQ5D+ANRO/wVp9IpdElzpODu5kIqypfKzapNB8yG1s8mLLw\nSJqceqTnp4NPx3qnXbAB+G+Oom2BAX2IXSrfloMLHs7YslE7uhy3lPKIo8Fd\nhUJ9tN6TZ5SWT3aryX/0M1oglP/4URoArSmQd42yWCYks+pKJpvzz89qesLM\nOZnJmD0/FUuFfDhfIvBLLjqXckzMNL0PBU3DaGXmVr13I0F+WrrFyFaQvowE\niFT71HQbPCJRHVCdieR8Fi4ufNLtaM+9d7ALEUK6lVTkxtWR482Difllzoey\nRPmrc8YZzKBCkeHHzGSFBg7JIT2BLOW7vWJlRPxXOMjigR49FhXpnQd2PMZo\nnqDOCfHWaEx7jriYWxsP9EssbHqMbz5MJjdpTHArL1HQrnSKGvnen5C49r5s\ntpHGMv/0ARNK5Iuzf46TnQtwSdwA+f44N57GtMlTsEHYxuen5A62RczuH2nP\ndRlD\r\n=75vy\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIH9Msl3VOS2IOfmE/TKlNsHwGKZyk3R02Ih0xeBRpjFtAiAKwK9yE8LSR31la4TBjagNQJ5S3wXBysqE7ve3HqulHg=="}]},"_npmUser":{"name":"kyeotic","email":"tyrsius@gmail.com"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tyrsius@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-9_1614134466404_0.5676592974458732"},"_hasShrinkwrap":false},"1.6.0":{"name":"dynamo-arc","version":"1.6.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"c9de0b864fdb03cfb457e4c2d609b08049d23b5e","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.6.0","_nodeVersion":"14.16.1","_npmVersion":"7.11.2","dist":{"integrity":"sha512-3gsAvgBvBjgptbpEPGXj+rNkrjQUH4okg1P6MjoQktw4bFvvm9R5NCrn+EWGt0zYwXtC65i0+MiLomaBW7pCDw==","shasum":"7bcd54196cb8559682efcc6c2b802ec64160204b","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.6.0.tgz","fileCount":8,"unpackedSize":45458,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhCWYCCRA9TVsSAnZWagAA3jsQAKMM7wNU5n8EBcOP9TCG\nmXe2hQNqKVKauBRRHx1QMqNKFEsK+YeC4UjTMNLpH0B2q1Y08jN3BoCwltbG\nc8IDgvFWi+qUXBUih3PkPuoVjvf0kTwQo7rtvd5XYYEvgn10ROV+Dj3dfdRk\naRy1ZpkWecn3qLl6PDJgL13eiqvYFWaNrRWqc+6kFW/t4dY8h5P3FoKOWSLR\nlxMoL4Xd5Tqgj2rYm+qxjiHvgBkjCoa7vBgt5qTOJdhlhczhTYkMUNiyxD/t\ntAYVppIhx7mjuLdKO+tYQaLbxaYByEc7BVGw4HG5xTJNyfDdKynDm7vSoSLT\neeuQCtS2V+2EL71A/YxTsD2//FwnprUQGbKhveJBUbq5QJ6d+afujiiDkXyD\nSPrFe9M5voEv7W0j5HuOCqauR1bK3oUAfrE9z0dNZDc3uducAhsHWyiqDBUJ\n0WgmHZYwX/6lYNmO017wLHXxAvlSar7B7xWGyq13hIUllpF8wQHJh9LVjYUg\nSpQ3cH7S5QcpiLDZytqZqIIlujVIXtWmTwQOzZNyFhJpKukuU7Sqt/z7iXLh\n9E3MWSEvGBr4A5M+RJfIMEp3kiqUSK9bmiMkTLF0670d4TnzgwWh5LUH8RxU\nF0EZGF9I5c6vBt2shL6nIuoB4+F+6dvJWOraSjfkQbU8XtGrfAIMQ0lIlnnJ\nUwGu\r\n=rC2Y\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDOs8wVHOgUrdaD4LZxxxadtISdMKQrDSulWL7unc0iBQIhAMeCwozxxI677b/Qf+VgIpRfJMLfKCCeZBYoZdvToePu"}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.6.0_1628005889893_0.9720915079750883"},"_hasShrinkwrap":false},"1.7.0":{"name":"dynamo-arc","version":"1.7.0","description":"dynamo data client with async-friendly API","main":"src/index.js","scripts":{"style":"prettier --config package.json  --write \"{src,test}/**/*.js\"","lint":"eslint -c package.json \"{src,test}/**/*.js\"","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"ava","test:watch":"ava --watch","test:coverage":"nyc --check-coverage --lines 80 ava","test:coverage:open":"npm run test:coverage; npm run report:open","report":"nyc report --reporter=html","report:open":"npm run report && open coverage/index.html","test:ci":"npm run check && blue-tape test/**/**.spec.js | tap-xunit > xunit.xml && blue-tape test/**/**.int.js | tap-xunit > xunit.xml && npm run test:coverage && npm run report:ci","release":"np"},"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","dependencies":{"dynamo-butter":"^1.1.1"},"devDependencies":{"@kyeotic/eslint-config":"^1.0.2","ava":"^3.11.0","aws-sdk":"^2.722.0","eslint":"^5.16.0","nock":"^13.0.3","np":"^6.5.0","nyc":"^15.1.0","prettier":"^2.0.5","sinon":"^9.0.2"},"prettier":{"tabWidth":2,"semi":false,"singleQuote":true,"printWidth":100},"eslintConfig":{"extends":["@kyeotic/eslint-config/node"]},"gitHead":"754e524b5c88e65405e9471b115dbfa86f17eb65","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@1.7.0","_nodeVersion":"14.17.4","_npmVersion":"6.14.14","dist":{"integrity":"sha512-47eGtlDdu5z7ZTLbKVBXVK5fBiL8YZBHtmbSDlQxfprsohYSgxWHjUhdsCQU9ZIhY8W2g7O1g7XlAXqzM0jUeg==","shasum":"6a4e87ca281bdbd9fee64d83ffc1353f4156daad","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-1.7.0.tgz","fileCount":8,"unpackedSize":47526,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhFAiKCRA9TVsSAnZWagAAsNIQAIV6ptEQZdbdCRzM6JC7\npX+LhBUzrqufXb1WTgdbwYnZy9yWHrnXVkVTzBFJ7AGVkPCs9FV+D2kDnhrb\nlnHy6NDGrZ5UMJ8XgzNxMLl2fyeTZ/7RfOt1UPz5E0pEsJ1PndTV3mVzFq8Y\nlo+QneEAqIMekGf65dCK3e6oWhefcbzsUgO80Djpokrrl3WXf7WOGx1gdZkX\nhs59awu6m3KJwlnMycpcPNoNF12IvqenJsqKaf/iQEUsOHTSts7sfcvvTH09\noRLsXgswutSd+nAZA7z49w2RrtWUiqrjhEfWwL5HIfOSsYUoMmWDpZqgdfHC\niEGWBTO/k6wjur4PJFH2iPfK5H0FiRuVS/j5aizQ6khv8zCD5xDiY9ZhZOQd\n/nXUk11cVdE3Yte0eoeMP6gLzXPgYUnOIeEvydLYW3FchZOnPeGJRHEHIyqy\nAjfYJyecjROPE8u6f9L2u5A3klA4t/K8od4xMp/+r5qcQNxx5WrraY2za7Nb\nkd638qwyVEgbQiN7PbSoqpKN/pPRcnavN+QErYij7SI4ZxDyb7UI4s1+hs8s\nIr7L/EVqy/4n6G5MHFpo0lk1IVIv4r3CU369r5FOTbmC77u9KpVqEeHGNp0W\neJ1/UrouryI+lpGdTR5UQEXcbwmogPS3z1Hb5CUQhAes4Xtt+AiZovZ+IvbO\n1p12\r\n=rcee\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDAeyQMpC7x2HQsX07VMXpy2LJLctC4gcuerXZHyU1cwAIhAPPd+IughTpTnZ4LvdtDeA/Nn0fyTNHNXSwe3cBa1TmS"}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_1.7.0_1628702857982_0.7316733261196617"},"_hasShrinkwrap":false},"2.0.0-10":{"name":"dynamo-arc","version":"2.0.0-10","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc","build:tsc":"tsc","build:clean":"rimraf lib","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@jest/globals":"^26.6.2","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.4.2"},"dependencies":{"@aws-sdk/client-dynamodb":"^3.28.0","@aws-sdk/lib-dynamodb":"^3.28.0","@aws-sdk/types":"^3.25.0"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"7207cd747541752f565b50a4744be87c1f1f714b","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-10","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-2ruBcmhXrUcn552QvHcaITfbFxYHRNFlkqtOhof+jBGtt9Z//FZlUoLAaXlWo4aTuNaOTjK/SrHXclmrOabWQQ==","shasum":"298f38e8dba59520ca26e7f42834819c5441ef30","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-10.tgz","fileCount":21,"unpackedSize":95380,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhON6YCRA9TVsSAnZWagAAmx0P/1KMwGdNhD3lLciL8ICQ\n52QKuVQwjsrv+TgIJfnNLyzkrnwqRDBeRU3sjlVyUFi/FdWoRAPh+50D6jAn\njKBtZNY3NeJTvGs6qwhYLH+T08L7f0DGAMfb1Tp7ISfHMu4+cw8VOTbduWqx\nDDWrD5P7AS+a9d35u6KYJE3Jyi77JInOsa2rzSYFpbFMe9hs9zAvsCOgzAnx\nVsJtqJc3KRLC/RnqBxE5NAwQKt/jXUtB/Pp1V/1l1qIItUcuZfBNgPMPACb7\n5oNqhIQdHpIb8Cm9DC5IguC51cQdixZTgcDuxxCAI3iLeHF3oAXfEmSk7oXz\nCpaEE63jzQwsng/FvGXTXRtl9+ILvCBVtXXs7tZlt+XhpyzdLnESiIdzTVKn\ncUY5EYZ5NeAkb/i+Z80cS6MJvBJMGp61H4/6iDzUGMl2osDoAjii6/b/cuyv\nAVrrhl2Jv4P9b3LzMrkZya7LuDxUn89YCr/gOM1CDqgwSELTgUgn9INi/arQ\nZakudPn5ldWiHGJAJ2cRlvjB+Y+Dk9ZaXsF085/rC74j78bnisfdyfejqYgy\n84uU5RiJpSVLwwms1Aj3/LGzPvSUTEGpQEcY7eeXVFy3wUwTkzs4TNk69MJG\nRIvBRj4KBF06bAENliBcX5VpY6jGaT3mT28kJSOWTQRxAZoNi66UjUKSgoXF\ngc4h\r\n=g23X\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCICqN2GRMchXIQGm6Lc8W63vP8lyS/8EnrIWo513ufB23AiAAzukZWZsqf/S0UWv4qfc63xwEOF1W3ajfGUyI7eAYUQ=="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-10_1631116952673_0.5327473784302625"},"_hasShrinkwrap":false},"2.0.0-11":{"name":"dynamo-arc","version":"2.0.0-11","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"^3.30.0","@aws-sdk/lib-dynamodb":"^3.30.0","@aws-sdk/types":"^3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, BaseStore, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  dynamoConfig: { region: 'us-west-2' },\n  tableConfig: { tableName: 'my-datastore' }\n})\n\n// Define a type-store\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `BaseStore` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  dynamoConfig: DynamoButterConfig,\n  butterConfig: ButterConfig,\n  tableConfig: {\n    tableName: string,\n    idField = 'id', // partition key of the table\n    sortField = 'sort_key', // sort key of the table\n    typeIndex = 'type-index', // index used for the TYPE value\n    ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n    hasTtlField = true,\n    hasSortField = true\n  }\n})\n```\n\nThe configuration for the *dynamo client's* `dynamoConfig` is passed to [Dynamo Butter](https://github.com/Nike-Inc/dynamo-butter) using the [Configuration-Passthrough Mode](https://github.com/Nike-Inc/dynamo-butter#configuration-passthrough-mode). Use the same values you would use for the *DynamoDB DocumentClient*. The optional `butterConfig` prop can be used to control the second config parameter to Dynamo Butter; this is most useful for disabling keep alive.\n\nThe only required property for the `tableConfig` is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\n## BaseStore\n\n```typescript\ndeclare class BaseStore {\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `BaseStore` are the `dynamo` client, which must be the result of the `makeClient` function, and the `type`, which is used to create the composite key for the record.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends BaseStore {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface StoreKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface BaseStore<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  delete(id:string, sortKey:string): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  put(item: T): Promise<T>\n\n  /** Execute a query against the configured Dynamo table */\n  query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `BaseStore`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends BaseStore\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"cc0fc09b1ffbca0533690292d9aebb57ba3628b7","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-11","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-0I4s6PjEuPsUEkpcIOnuRn6R1cZvVahUxxGsttk6noWICwtYiFA79veC8ppEn3PAcHvcBghTEJag7TF2ldIR0g==","shasum":"5516a4492571ed420d2fea0392ce07847d5ac961","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-11.tgz","fileCount":10,"unpackedSize":640516,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhOomYCRA9TVsSAnZWagAAllMP/jxBO6GUO0GmsfDk6CXN\nJuPkFLyL3wS02NeJc015zxkpP6x0GfjO4ibhymZ1byHR1IcwwsyciKRgkVq4\nNiwfllHn831gkB8nSE9zp8I+nDqHB4gcAvZm9QydwW0kUY1yqwgUQkho8bLu\nEfNhjJZWXGmdMpVjShkP3MYvWXBWspjfqj0H4X4gFAUHuTkrah1d2gT67seP\nl0MnXURcAfXJ+6fka0luXt/u8ip+1Thl8a/s958qPV3Iq8yILtOHpoJkZV98\ns8gWJZe5nOEtSdjZc5UHZKFBEbnRvXJEYC+BV+/lFHrurleRpg7I5s6BbuDG\nWOi17IiJYYEm9mBfSE3+DwHI5e6fpr9n6pwP4f8XJMybNWB8hmZUNH1IHrnl\n/13GtUmjk4hEChjtWNCcsvXPmnVVxTLGjvR9GNFxpaezNrSzho8xbM2muRHD\nr1Uk8bB0OYvQwgdofwjJiFbvHLWmUd+/t2x+nqI1ut4pIhAXTfIiwoZxUP5r\nI55sMJdCi004u53u1TwlncUMZVvUfJTrKUdpZfWpOMOYPrs1xjy1brukrJkZ\n+8CxE6wDqLjfGRYw9iXr53HyAjCdFcxkkrrzhr0FQ7YBazIF26nOtmp5Zbax\neWf4rSFmUDam0hJhu7iWgcS9l35sE8ySHiU4676et4sZT62fE+6cnaPONl5t\nK81z\r\n=j6rZ\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICrvEpGEU/kjeZEYNrsf4FCEbMXoi1IADwAbenKJv3w8AiEA0/gUf4zibaVXGiP9iitx8jSAV/iwucef3DaOn+aKIy4="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-11_1631226264602_0.5923448191042844"},"_hasShrinkwrap":false},"2.0.0-12":{"name":"dynamo-arc","version":"2.0.0-12","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"^3.30.0","@aws-sdk/lib-dynamodb":"^3.30.0","@aws-sdk/types":"^3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"06b857d298d94b275c2489011d6c3f0ee339ed21","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-12","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-vrvusA1MuQe9jPTq735+BFUWb9/5iaCnHst9vUcHew7MuZRpUoii6hwnA+Fws0R8SVNYIquWIrX/BZfKvGIRvQ==","shasum":"cd492f38d40f05cdd32629c34f87f65daf3d237c","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-12.tgz","fileCount":10,"unpackedSize":641785,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhQM6QCRA9TVsSAnZWagAAT9wP/3Cfbh1L8do5IMuKnYsj\nm7G5DZa5nf2UcRVS6IwmEvrmfXdSu3Oz4AfCEODCdZf5iQpeZ3u71Oq+hcOv\n7cZob2hx5UKk1r9CxmG17B7R0Km6M7CVUrxmLBQ2TrOWyQeniiBXmSztLAzb\ni4T++8dVOy4RAgrvrWotcbos2HXPfCiPrLkGFWVxmsu6A+4Qa7nxtBYY17IA\nnXYZ/AU8yHQcdVLtkP+wg92vQCkgX31KyfZ0lnWYd3t0qb7VEcimhtdTCy9/\nSacZnFiTrDZDo0+/xt504FgSzqSdHmF04d28fmTtEiu0t+krtMeIQL6xGohm\nmZBofGT39Q6dtSNi1Bd/g1mq4+d1hBtCXm8l/OiEEV5G5zQKmdNnzfCUUm1P\n1uAkV+USYxsH1s8qcsePGBStOPl8zaKliIqx7NNX7GlHFVyU5GC0R5Bt9B2V\nA/MHVkc6tmUWFFkTEkddaJDsM5dRH1ALR3ap9YjNHvmrap66DMf9LhWWhoZY\neACHVrliEATBBUoJTToKpi2BhLBYQT48Mrflq9u+RdDxBDyKZy1S3/naEEWd\nl+JbBoymNlRph0IsPuLpFS8YCggjMfZsoPH448qaDFX1r/J6jDOaBwDmpL5T\nisfwYd/isJQYNHa4KXXG+4lELqSSEeTI0D7eK2YbEyFXaEMU2F1Bn90Lu/Gq\nNgs7\r\n=VlVV\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDXYg7GDIR1L8oNEWJIrUBIAUYzeUEkmgUt/DUbSL2+9QIhAJOkNPE98Qp0wsK4oJVoXAacqZL3MBQbY5zZEwHwiPE9"}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-12_1631637136152_0.7125396578888876"},"_hasShrinkwrap":false},"2.0.0-13":{"name":"dynamo-arc","version":"2.0.0-13","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"^3.30.0","@aws-sdk/lib-dynamodb":"^3.30.0","@aws-sdk/types":"^3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"a7e61e62dd35624188ea5c09b186d03c26ff2193","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-13","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-2OwU3LbQDijJVLqQSahIEk8CHJf5Q8RmXKVhMwbiVEtgGMldulzggFOZOCAOqY+rp/olLtqY+rvSXgZTMqQa0w==","shasum":"7bedbf13edc29659e018804a20e06416d16d730c","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-13.tgz","fileCount":10,"unpackedSize":642215,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhQmOGCRA9TVsSAnZWagAABs8P/juzTn7K5qtPMiKrvGQp\nZKxz74j5gLw2FXJYICgVGxWbJTcGvp9KEhJptmRucEoOk5+ZzhfMG56tFCVg\nGNFfigjM2kZFVpNrusMk3+i9972784p9BRA2e3QaK0MxIk9MJbFVq2zWluXz\nx7EjlwppK7n+s3l6UuvtdTs4X1enZpiAZNq4YZdFkWGxbNWgVi0ActQkV+ZW\nt8sbwbegIExBa7dUxyxcE3cejRJZicvTct06J2bCJdS0gxW/LpbkEVT5nzhv\nkZrvpgc6F3r9N3Dfdgt7QNAu/+Bu5sywg5RcZfpHZLvIiA8zRrPiNI2X3IcZ\nQ75T3/znmzF3VMoVCYTf+jeradz6cTuizfoszymL9qjQa5ZXeCXzcO5D2wZf\n2/MRGc4sFZZkwlplDVBcRcHG3U3oQueKNoa+hUf6PBCahREnZybIRJswSwMp\ni4ogFv2A8NbTxgKozjoKrbrVvpGCKf2m5V7KMhnG+cSwjogPgiFgL9tzVEJU\n040J55A87JkGdhlzFNEd835lH61irEsPc7LFXmLMgaxZjwdsEJDhgaxqa3OY\nm5KG5QUC+517f3qQdCDTuK3V7TrD5jPBPioNLw2wOHsRmu4QPD/IY3kDcwns\nJ9dFU417djPRi+nnvCUbKmoDejwZpf8eXuYQvE58jG/9pJE+EVIrC6OmnDU+\nBELj\r\n=zsL7\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIBYo7LLEeha02t2DtvMAS2UioS68cylxWlVf9oOA9wTdAiEArozw0b6fcq6ctExCK5KVR6AqvtDTsbl6EjHn75TvtyI="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-13_1631740806344_0.014735239310852855"},"_hasShrinkwrap":false},"2.0.0-14":{"name":"dynamo-arc","version":"2.0.0-14","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"^3.30.0","@aws-sdk/lib-dynamodb":"^3.30.0","@aws-sdk/types":"^3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n\n","readmeFilename":"README.md","gitHead":"8e9024c00b3e8780299102e8993caa90b700a48f","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0-14","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-69M8Femw5+zgj9D1Yn1IFaqhexHzJJHJ2nW8PU/3t/2U52jml3El7CSNQUD+Msn+G1nezPtR0S/BNlis9cx+5Q==","shasum":"9f0ca09617465e9777b8dc646a7275421666da18","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0-14.tgz","fileCount":10,"unpackedSize":642237,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhQ6JZCRA9TVsSAnZWagAAdRsP/Rp7RNj/0eZp0ScgIX3C\ni5EN5h77qeeIrRc/Y4j8/OXzyr9n4tguLI909cyI9hIID/GZJPy9f0/dU/Xk\nb9nS2Mg+Hy/ucKCJWkLDHk9k8vOubMKExJlyyPd5KKrkyo+IioMGn9sbq7ib\nUcMwpefLAAnO7mJeURpGqlPXeclskFWDv9uzyp+Pf3qMtmuFXiZMctRSUjye\n31zb03FQaDfNow0QQW3i9o/ZFkslVMusp3jB9X1T4ECKvdKVY2FcvprsNZjI\n/dZE+pYKyNFyZfkid4KAmQLjjluPW44FU9ZhSyUl0xZtm+sbLtqb0LiKNoaW\n4UeqhZwmpuf+CV/YbqJcBemgLQDDCvRKoknadNl2HrC6jqbg19ppeb6xYy83\nvKx747r9dhQhno0LFjw6PkFHIedF8vNqaSFSP/KNVoIkzK8aHYKlphx5/Q0i\nnopxTGE3V7j3raL+wdYMnqt9L/UPveSbR4O0YfZ8K0/epbEJuzQRaUX2G6Tk\nTVP/zvyP5DcEeDT3DQXDufrBBak9x9OGH3oG3mSikESg5xaGYkjyUfaN1rK7\nBJqX7pRs+cy9x8pO+xQxvyktZnLWOhUVBrwvOIZXGxrK9FE3fGKCYvSWtfZ0\noySm67tfHIDdRgUCvsTq8n8khuEMhALl9HlEqneyT+HqJaqywZG86FsOPX2J\n8O6S\r\n=/ru5\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCRcEukuk2cdIQC9hnVEybctc7940zxZffRaz4hSBylRAIhAMMXIpGfl4K5bvhxKH7zEfOGA3zKRQcC3k8OTwYmUqk/"}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0-14_1631822425418_0.904670888449417"},"_hasShrinkwrap":false},"2.0.0":{"name":"dynamo-arc","version":"2.0.0","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"gitHead":"0772e010607e41ab0c22471f1a9ffd6e4e32415d","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.0.0","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-qgFy4GgyhIxDNX/6v265Wjtu0PNt9LbA8wTdzUQV04maz9qE6NMH1WMMXYN9GwdlU25Ycutds1ni4XyQx+7Niw==","shasum":"aeb6776f1b67d81af2d246a2e901efc85d8cce5f","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.0.0.tgz","fileCount":10,"unpackedSize":686718,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQCF3AyWZ2Vdbc8Al2dl+IIxegTEXHB7Oi0M5juQjzlAhAIgIssw2u7f8VGsAuKO4PMCOXLTzO6oLDESAdw6BDoq5+8="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.0.0_1634313210128_0.9509347046737091"},"_hasShrinkwrap":false},"2.1.0":{"name":"dynamo-arc","version":"2.1.0","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^26.0.20","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^26.6.3","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^26.5.1","tslib":"^1.13.0","typescript":"^4.3.5"},"gitHead":"40c40ea18e7e8f47e2364fffab760d592e9879b1","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.1.0","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-DLtRlQk2TC/gozMq2FufUlXVBdb26Yx9V+b65FTt96/YinTwspDIZA3Jish9U/E0FdKUoXYq9Cf8W+MmN0J+TA==","shasum":"f679a23b21e86181b1d171e93eada58e8b3a1fd8","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.1.0.tgz","fileCount":10,"unpackedSize":686769,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIFRtF0XaiicVVvUZ2Nqzqs1/lcoum7quYviDhIk8Nm71AiEA1kyRqIE7Ok+Y5q5Vt64roaV53wpQ6Hm/Jy9lytkB0P8="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.1.0_1634586786092_0.679274989347491"},"_hasShrinkwrap":false},"2.2.0-0":{"name":"dynamo-arc","version":"2.2.0-0","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^27.0.2","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^27.3.1","jest-watch-typeahead":"^1.0.0","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^27.0.7","tslib":"^1.13.0","typescript":"^4.4.4"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n## Stream processing queries\n\nthe `store.queryByPage` function provides streaming access to the results of a query. Since only one page is brought into memory at a time this can allow queries to be processed that might otherwise cause the process to consume more memory than its host can provide.\n\n```typescript\n /** Execute an automatically paged query by processing one page at a time */\n  queryByPage(params: DynamoParams, pageFn: T[]) => any)\n```\n\nThe `pageFn` is passed an array of `fromDb()` mapped rows and its result is awaited. The paging process can be halted early by returning the `storeSymbols.pageBreak` symbol from the `pageFn`.\n\n**forEachPage** is an older, deprecated method that functions similar to `queryByPage`, but uses a pre-canned query that pages the entire `typeIndex` for the current store. This behavior (and more!) is possible with `queryByPage`.\n\n## Modeling Relationships\n\nIn Graph Theory an [edge](https://en.wikipedia.org/wiki/Glossary_of_graph_theory#edge) is a relationship between two nodes. Since DynamoDB is a NoSQL store there are no native relationships, but they can be simulated by creating records that use the _primary_ and _sort_ keys on the table. These records are called _edges_. Arc has tools for managing _edges_ modelling either parent-child relationships (one-to-many) or associative relationships (many-to-many).\n\nThere are three classes for working with _edges_.\n\n* `ChildStore` - Used for managing children in a parent-child relationship\n* `EdgeStore` - Used for managing edges in an associative relationship\n* `BaseEdgeStore` - Used for managing edges in an associative relationship. This Class is intended to provide additional flexibility for cases when the safety checks or automatic edge-selection on the `EdgeStore` or `ChildStore` are too restrictive. When possible prefer the `EdgeStore` or `ChildStore`.\n\n### How it works\n\nHere is an example of a parent child relationship as it would appear in Dynamo\n\n```javascript\nconst node = { id: '_USER_:4332' }\nconst edge = { id: '_USER_PREF_:4332', sort_key: 'volume', /* ... */}\n```\n\nNotice how in the edge-record the id is the same as the User ID. This allows a query to select all of the _user preference relationship_ by knowing only the User ID and the type (`_USER_PREF_`).\n\nHere is an example of a two-way relationship\n\n```javascript\nconst user = { id: '_USER_:4332' }\nconst team = { id: '_TEAM_:8867' }\nconst edge = {\n  id: '_USER_TEAM_MEMBER_:4332',\n  sort_key: '8867',\n  gsi1_key: '_USER_TEAM_MEMBER_:8867',\n  gsi1_sort: '4332' \n  /* ... */\n}\n```\n\nHere, again, a relationship to one node is stored on the primary key. A secondary relationship is modeled on a GSI, using the same method. This allows a two-way, or \"many-to-many\", relationship to be modeled.\n\n## EdgeStore\n\nThe `EdgeStore` simplifies working with many-to-many relationships between two types. It requires the `dynamo` client to be configured with a `sortField`. It must also be provided a `secondaryIndex` in its constructor to use as a GSI for selecting secondary edges.\n\n**Associate Relationship Configuration**\n\n```typescript\nimport { EdgeStore, EdgeStoreSubConfig } from 'dyanamo-arc'\n\ninterface User {\n  id: string\n  teams: Team[]\n}\n\ninterface Team {\n  id: string\n  members: User[]\n}\n\ninterface UserTeam {\n  userId: string\n  teamId: string\n  role: string\n}\n\nclass UserTeamStore extends EdgeStore<UserTeam> {\n  constructor({ dynamo }: EdgeStoreSubConfig<UserTeam>) {\n    super({\n      dynamo,\n      type: '_EGDE_',\n      idKey: 'userId',\n      sortKey: 'teamId',\n      secondaryIndex: 'gsi1-index',\n    })\n  }\n\n  // The EdgeStore makes its type-generic methods \"protected\"\n  // in order to force sub-classes to provide type-specific names\n  // Users of the UserTeamStore shouldn't have to track which of \"user\" or \"team\"\n  // is the \"primary\" or \"secondary\" nodes, they should be given clear names\n  async getTeamsForUser(userId: string): Promise<UserTeam>[] {\n    return this.getEdgesByPrimaryId(userId)\n  }\n\n  async getUsersOnTeam(teamId: string): Promise<UserTeam>[] {\n    return this.getEdgesBySecondaryId(teamId)\n  }\n}\n\nconst userTeamStore = new UserTeamStore({ dynamo })\n\nconst teamsForUserX = await userTeamStore.getTeamsForUser('X')\nconst usersOnTeamA = await userTeamStore.getUsersOnTeam('a')\n\n// add user X to team b\nawait userTeamStore.put({ userId: 'X', teamId: 'b', role: 'member' })\n\n// remove user X from team b\nawait userTeamStore.delete('X', 'b')\n\n// force team b to have the following users\nawait userTeamStore.syncEdgesBySecondary('b', [\n  { userId: 'X', teamId: 'b', role: 'member' },\n  { userId: 'Y', teamId: 'b', role: 'member' },\n])\n```\n\n> The `EdgeStoreSubConfig` is a utility type that simplifies the config of `EdgeStore` sub-classes by removing the properties that sub-class constructor would hard-code for the `EdgeStore`\n\n## BaseEdgeStore\n\nThe `BaseEdgeStore` extends the `Store` with a pair of methods that assist with creating a `batchWriteAll` call to add and remove records. It is used by the `EdgeStore` to implement the `syncEdgesBy[Primary|Secondary]` and `getEdgesBy[Primary|Secondary]Id` functions. If the `EdgeStore` has an implementation that does not work with your model you can implement your own Edge Store on top of the `BaseEdgeStore`.\n\nThe `BaseEdgeStore` implements the following\n\n* `async syncEdges(dbEdges: Edge[], edges: Edge[]): Promise<[Edge[], Edge[]]>`: Filter the provided edges and execute a `batchWriteAll` against Dynamo.\n* `protected filterEdges(leftEdges: Edge[], rightEdges: Edge[]): Edge[]`: Used to filter edges in the left that are missing from the right. The default implementation compares the keys defined for the store.\n\n### ChildStore (Coming Soon)\n\n> The ChildStore has not yet been released due to challenges with its API design. \n\nThe `ChildStore` simplifies working with the children in a parent-child relationship\n\n**Parent-Child Example**\n\n```typescript\nimport { ChildStore } from 'dyanamo-arc'\n\ninterface Parent {\n  id: string\n  children: Child[]\n}\n\ninterface Child {\n  id: string\n}\n\nconst childrenStore = new ChildStore<Child, Parent>({\n  dynamo, // requires `sortKey`\n  idKey: 'id',\n  type: '_PARENTCHILD_',\n  childKey: 'children',\n  parentIdKey: 'id',\n})\n\nconst child1 = { id: '1' }\nconst child2 = { id: '2' }\n\nconst parent = {\n  id: '1234',\n  children: [child1, child2]\n}\n\n// Save children to DB\nawait childrenStore.syncEdgesByParent(parent)\n// OR\nawait childrenStore.syncEdgesByParent(parent.id, parent.children)\n\n// Modify children\nparent.children.pop()\n\n// Remove last child from DB\nawait childrenStore.syncEdgesByParent(parent)\n\n// Fetch children from DB\nconst dbChildren = await childrenStore.getByParentId(parent.id)\n```\n","readmeFilename":"README.md","gitHead":"d7a61fe90f5fd3eaec22131880cf2c5ae48a52ce","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.2.0-0","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-O/2V6ttWUIbWpQkW6QvL344GMpAwC12nJC2QTYGMSBga+GAyRp44Ge0MMpB+k2g4OPvEJacix0hLxdn5yIwChA==","shasum":"9f9a491ab88960b1fc41e6f67165ea38579a3d63","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.2.0-0.tgz","fileCount":13,"unpackedSize":697917,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDD1gH2XcIIxVdm7kq1irBbOWkKdhC/nUAvV23iBX9o6wIgBi6OTXGU8seTju27XDw1LQtlzREK3uT/THPVlJtvXls="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.2.0-0_1636691522515_0.23896713680204207"},"_hasShrinkwrap":false},"2.2.0-1":{"name":"dynamo-arc","version":"2.2.0-1","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^27.0.2","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^27.3.1","jest-watch-typeahead":"^1.0.0","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^27.0.7","tslib":"^1.13.0","typescript":"^4.4.4"},"readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n  })\n}\n```\n## Stream processing queries\n\nthe `store.queryByPage` function provides streaming access to the results of a query. Since only one page is brought into memory at a time this can allow queries to be processed that might otherwise cause the process to consume more memory than its host can provide.\n\n```typescript\n /** Execute an automatically paged query by processing one page at a time */\n  queryByPage(params: DynamoParams, pageFn: T[]) => any)\n```\n\nThe `pageFn` is passed an array of `fromDb()` mapped rows and its result is awaited. The paging process can be halted early by returning the `storeSymbols.pageBreak` symbol from the `pageFn`.\n\n**forEachPage** is an older, deprecated method that functions similar to `queryByPage`, but uses a pre-canned query that pages the entire `typeIndex` for the current store. This behavior (and more!) is possible with `queryByPage`.\n\n## Modeling Relationships\n\nIn Graph Theory an [edge](https://en.wikipedia.org/wiki/Glossary_of_graph_theory#edge) is a relationship between two nodes. Since DynamoDB is a NoSQL store there are no native relationships, but they can be simulated by creating records that use the _primary_ and _sort_ keys on the table. These records are called _edges_. Arc has tools for managing _edges_ modelling either parent-child relationships (one-to-many) or associative relationships (many-to-many).\n\nThere are three classes for working with _edges_.\n\n* `ChildStore` - Used for managing children in a parent-child relationship\n* `EdgeStore` - Used for managing edges in an associative relationship\n* `BaseEdgeStore` - Used for managing edges in an associative relationship. This Class is intended to provide additional flexibility for cases when the safety checks or automatic edge-selection on the `EdgeStore` or `ChildStore` are too restrictive. When possible prefer the `EdgeStore` or `ChildStore`.\n\n### How it works\n\nHere is an example of a parent child relationship as it would appear in Dynamo\n\n```javascript\nconst node = { id: '_USER_:4332' }\nconst edge = { id: '_USER_PREF_:4332', sort_key: 'volume', /* ... */}\n```\n\nNotice how in the edge-record the id is the same as the User ID. This allows a query to select all of the _user preference relationship_ by knowing only the User ID and the type (`_USER_PREF_`).\n\nHere is an example of a two-way relationship\n\n```javascript\nconst user = { id: '_USER_:4332' }\nconst team = { id: '_TEAM_:8867' }\nconst edge = {\n  id: '_USER_TEAM_MEMBER_:4332',\n  sort_key: '8867',\n  gsi1_key: '_USER_TEAM_MEMBER_:8867',\n  gsi1_sort: '4332' \n  /* ... */\n}\n```\n\nHere, again, a relationship to one node is stored on the primary key. A secondary relationship is modeled on a GSI, using the same method. This allows a two-way, or \"many-to-many\", relationship to be modeled.\n\n## EdgeStore\n\nThe `EdgeStore` simplifies working with many-to-many relationships between two types. It requires the `dynamo` client to be configured with a `sortField`. It must also be provided a `secondaryIndex` in its constructor to use as a GSI for selecting secondary edges.\n\n**Associate Relationship Configuration**\n\n```typescript\nimport { EdgeStore, EdgeStoreSubConfig } from 'dyanamo-arc'\n\ninterface User {\n  id: string\n  teams: Team[]\n}\n\ninterface Team {\n  id: string\n  members: User[]\n}\n\ninterface UserTeam {\n  userId: string\n  teamId: string\n  role: string\n}\n\nclass UserTeamStore extends EdgeStore<UserTeam> {\n  constructor({ dynamo }: EdgeStoreSubConfig<UserTeam>) {\n    super({\n      dynamo,\n      type: '_EGDE_',\n      idKey: 'userId',\n      sortKey: 'teamId',\n      secondaryIndex: 'gsi1-index',\n    })\n  }\n\n  // The EdgeStore makes its type-generic methods \"protected\"\n  // in order to force sub-classes to provide type-specific names\n  // Users of the UserTeamStore shouldn't have to track which of \"user\" or \"team\"\n  // is the \"primary\" or \"secondary\" nodes, they should be given clear names\n  async getTeamsForUser(userId: string): Promise<UserTeam>[] {\n    return this.getEdgesByPrimaryId(userId)\n  }\n\n  async getUsersOnTeam(teamId: string): Promise<UserTeam>[] {\n    return this.getEdgesBySecondaryId(teamId)\n  }\n}\n\nconst userTeamStore = new UserTeamStore({ dynamo })\n\nconst teamsForUserX = await userTeamStore.getTeamsForUser('X')\nconst usersOnTeamA = await userTeamStore.getUsersOnTeam('a')\n\n// add user X to team b\nawait userTeamStore.put({ userId: 'X', teamId: 'b', role: 'member' })\n\n// remove user X from team b\nawait userTeamStore.delete('X', 'b')\n\n// force team b to have the following users\nawait userTeamStore.syncEdgesBySecondary('b', [\n  { userId: 'X', teamId: 'b', role: 'member' },\n  { userId: 'Y', teamId: 'b', role: 'member' },\n])\n```\n\n> The `EdgeStoreSubConfig` is a utility type that simplifies the config of `EdgeStore` sub-classes by removing the properties that sub-class constructor would hard-code for the `EdgeStore`\n\n## BaseEdgeStore\n\nThe `BaseEdgeStore` extends the `Store` with a pair of methods that assist with creating a `batchWriteAll` call to add and remove records. It is used by the `EdgeStore` to implement the `syncEdgesBy[Primary|Secondary]` and `getEdgesBy[Primary|Secondary]Id` functions. If the `EdgeStore` has an implementation that does not work with your model you can implement your own Edge Store on top of the `BaseEdgeStore`.\n\nThe `BaseEdgeStore` implements the following\n\n* `async syncEdges(dbEdges: Edge[], edges: Edge[]): Promise<[Edge[], Edge[]]>`: Filter the provided edges and execute a `batchWriteAll` against Dynamo.\n* `protected filterEdges(leftEdges: Edge[], rightEdges: Edge[]): Edge[]`: Used to filter edges in the left that are missing from the right. The default implementation compares the keys defined for the store.\n\n### ChildStore\n\n> The ChildStore API yet been finalized due to challenges with typing it\n\nThe `ChildStore` simplifies working with the children in a parent-child relationship. While it is possible to keep many children as properties in the parent document it can be useful to keep the children in their own records in order to\n\n* search for children records using their keys\n* simplify isolated updates to children without updating the parent\n* keep children that might otherwise exceed the DynamoDB record limit if stored with their parent\n* allow paging through children; for very large sets (currently requires manual implementation)\n\n**Parent-Child Example**\n\n```typescript\nimport { ChildStore } from 'dyanamo-arc'\n\ninterface Order {\n  id: string\n  items: OrderItem[]\n}\n\ninterface OrderItem {\n  itemId: string\n  quantity\n}\n\nclass OrderItemStore extends ChildStore<Order, OrderItem> {\n  constructor({ dynamo }: ChildStoreSubConfig<Order, OrderItem>) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      parentIdKey: 'id',\n      childIdKey: 'id',\n      parentChildKey: 'items',\n    })\n  }\n\n  async syncByOrder(order: Order): Promise<[OrderItem[], OrderItem[]]>\n  async syncByOrder(orderId: string, items: OrderItem[]): Promise<[OrderItem[], OrderItem[]]>\n  async syncByOrder(orderOrOrderId: Order | string, items?: OrderItem[]): Promise<[OrderItem[], OrderItem[]]> {\n    return this.syncEdgesByParent(orderOrOrderId, items)\n  }\n\n  async getByOrderId(orderId: string): Promise<OrderItem> {\n    return this.getByParentId(orderId)\n  }\n}\n\nconst orderItemStore = new OrderItemStore({ dynamo /* requires `sortKey` */ })\n\nconst itemA = { itemId: 'a', quantity: 1 }\nconst itemB = { itemId: 'b', quantity: 2 }\n\nconst order = {\n  id: '1',\n  items: [itemA, itemB]\n}\n\n// Save children to DB\nawait childrenStore.syncByOrder(order)\n// OR\nawait childrenStore.syncByOrder(order.id, [itemA, itemB])\n\n// Modify children\nparent.children.pop()\n\n// Remove last child from DB\nawait childrenStore.syncByOrder(parent)\n\n// Fetch children from DB\nconst dbChildren = await childrenStore.getByOrderId(parent.id)\n```\n","readmeFilename":"README.md","gitHead":"a1dbe18d40e778dafc4d9fd5756066935260cbe3","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.2.0-1","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-R7NQ7jzfpf73nLF+LMhO46jnVBsYxaxgVMblcnP8zelETYdlNiU4fAYtDtscHno4mlPNEoK3eWrejn08Xc1qTQ==","shasum":"b00d9f4305f4c1b421971ce7c8e03f0bb9905654","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.2.0-1.tgz","fileCount":12,"unpackedSize":697696,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIDNaye7D86pMKYqQaj28eAgzouevfCU+XEU+g4f7HMVgAiB2bd5BBzP42uJQdLf4AMzYwxa0ZqzZNy62x6No8lH/rA=="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.2.0-1_1636699134158_0.13766858297087192"},"_hasShrinkwrap":false},"2.2.0":{"name":"dynamo-arc","version":"2.2.0","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^27.0.2","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^27.3.1","jest-watch-typeahead":"^1.0.0","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^27.0.7","tslib":"^1.13.0","typescript":"^4.4.4"},"gitHead":"86b13468400c16e423ee692d82b4231cdd7fc6fa","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.2.0","_nodeVersion":"14.17.4","_npmVersion":"7.21.1","dist":{"integrity":"sha512-OTYFYISOrZd8kehfG8a2DEycIvJ7vOmmkszCjMQW6VT3+ZmdoWfFQzwTpydE6nujwE2exZ+EZPVuDJCoDgOmag==","shasum":"ed95908f0e9d1e35945485671d7da926f46d7a59","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.2.0.tgz","fileCount":11,"unpackedSize":697798,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJh2pEwCRA9TVsSAnZWagAAe8EP/AyQYAzpu7GAxrGKQ2cj\nTRgqk7vdHq6Q0axU6XywET7ljI35yK72YY19f0Y52t+xpmBqEZFRzIfpD2qw\nHc1fJ/vmsbSfzniS5BfcQfnVsCE6+AQZxfJ5DAeeisdUrm/z5aReU4c446CA\nsu94Pt0RIrG9t7yV3wUr4hnyiTSlHSNJmKg1ssz+97H6Uj5w4A9uY14zJkez\nd+diRSOt74z2hSbbfJq8AsZKyuvjtkKbbT2dTgEJqAw+wT9TinZpj8+CGHKJ\nMTskeqiYoyFQQ97sPSnlYOChnoCGKlQQhhFG2KsxLVX6ieCCqWPYaEYA5EU/\ntQD9L7akgDCrDo1UQ+DKDaw0lUtldzGbzPfe6AcRbleAmb4PewswGw6HN6ak\nKwyjivK6COeB0R+SHH7reqPf3iP2iQRaD1zoPAxPQAnzr/+W4EqCgg0ql4sL\neJGHoIxvgCb5kBgcBvpq6MD8/PxA7PTxL5kiYAdKlpNZOFTBwhpIV4TImc2A\nuMQE9mDirgFbs4SKy1Qt5VA1yLHKQOl5o/StkrRUICjYHQ2hpkEJD4NaP+VG\nD/svH/XDtlTcUoMPWrxB3dcD/Oyh5S3cx31iNkgYltSxNJvHPbfYpAauFjAU\nZutZD2eXOKr1UoOhrPkZwQdsDVRd0RpGFHGYLHm9kPQoVDvXhE2Ag2yXVRpG\nuuJZ\r\n=dg6B\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIA/rD37slyWHZh7YzYFUjKu29FGCDYr1NvH63ycgKrEJAiEA8t1KGVMGAJSzdGNa5yoIop1bE9GiI1tpeeJWsk7ENgo="}]},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.2.0_1636734404802_0.5129200679235855"},"_hasShrinkwrap":false},"2.2.1":{"name":"dynamo-arc","version":"2.2.1","description":"A dynamo data client designed for use with DyanmoDB Single Table applications","scripts":{"style":"prettier --write \"{src,test}/**/*.ts\"","build":"run-s build:clean build:tsc build:package","build:tsc":"tsc","build:clean":"rimraf lib","build:package":"rollup -c","lint":"eslint 'src/**/*.{js,ts,tsx}' --quiet --fix","check":"npm run style && npm run lint","test":"npm run check && npm run test:unit","test:unit":"jest","test:watch":"jest --watch","test:ci":"npm run test","test:coverage":"jest && open coverage/index.html","release":"npm run build && np"},"main":"lib/main.js","types":"lib/types/main.d.ts","repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"keywords":["dynamo"],"author":{"name":"Tim Kye"},"license":"Apache-2.0","engines":{"node":">=12"},"devDependencies":{"@aws-sdk/client-dynamodb":"3.30.0","@aws-sdk/lib-dynamodb":"3.30.0","@aws-sdk/types":"3.29.0","@jest/globals":"^26.6.2","@rollup/plugin-commonjs":"^20.0.0","@rollup/plugin-json":"^4.1.0","@rollup/plugin-node-resolve":"^13.0.4","@rollup/plugin-typescript":"^8.2.5","@types/jest":"^27.0.2","@types/sinon":"^10.0.2","@typescript-eslint/eslint-plugin":"^4.30.0","@typescript-eslint/parser":"^4.30.0","eslint":"^7.20.0","eslint-config-prettier":"^7.2.0","eslint-plugin-prettier":"^3.3.1","jest":"^27.3.1","jest-watch-typeahead":"^1.0.0","nock":"^13.0.3","np":"^6.5.0","npm-run-all":"^4.1.5","prettier":"^2.2.1","rollup":"^2.56.3","rollup-plugin-terser":"^7.0.2","sinon":"^11.1.2","ts-jest":"^27.0.7","tslib":"^1.13.0","typescript":"^4.6.3"},"gitHead":"ea17d115e92349ecc4b080df40f3e4e63f185e75","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"},"homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","_id":"dynamo-arc@2.2.1","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-KsFyPgscl05rmohXkZa1dC8VdebqOlQ7tw7QOtxZA2c/fb8iA0F/lKAediUI00F0wxwAgOidmp+ejQElKMdWHw==","shasum":"c27331a01393fbaa1d3c84faac0868e1366324b0","tarball":"https://registry.npmjs.org/dynamo-arc/-/dynamo-arc-2.2.1.tgz","fileCount":11,"unpackedSize":685421,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIHj8QvgPwm62vPKO9+XJwcQCmeZHzk8+/33D0fGwBSXzAiAyiyP9ZzQxt49NkNo0wwlhFw/dPEauxTid6BpRvTIlKg=="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJiWKnmACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmpokg/7BboyIC93MBj0c/xdu7djB8BO9sesdzXqsCf/ENqtEC0kxXiw\r\ngd7c7A/kRpSn9tyJQwcE09wl82VaYInwm62+jVwM2tl9jY976Kq7UvNHa7Wi\r\nzzQLizHFa8tBlM5mRT9HxoBUUenNbnqaSEqvaLyt2OfhJUG/OedyV84RDdAd\r\nQrWW7GiO7Sj/2TIs75E9i3Q+Qmo+Pxxl1DWzl1EOkzdZek+wj0i/J7fsQ1No\r\nZOBeJ9Hgbce72Uwlhdp9CXWCPjcb2z/EMUm1RyXybsR51rriaMSecpgIcXod\r\n1ul06pjiQtKxFldJJFhs0COmwLFsnkxBCg2hG96EBqvGL2rPIIY8GCT/cFGP\r\n3U78xxv9/lVY8LNchUNr+ycPHUwFR2yJcQAeAG457lzz1cyXAgOUDraqvMxg\r\nztcSnaz5OIq2295WEOgfMdwstY4+NKDmiQOWLTEDvnECrMq4Odu6nsm76ytW\r\nOdLbCFL1PKU5x674Q9xPh3fvYr+2WpFdauZo2+dwo3Ur0Pedp33GkbkSJSGp\r\nBtIGxVNur5icTC3AGqq2va+N1JxxSR0r5cNZ1tmIEAudOp72ngmVfZrM4inM\r\n4TJN1F4b/qOlMbYNe0asPu/4YdRuR9t/GUniFKI/Xvg7d7Tg3uh2domMhvot\r\neWbigpa0N0Sw+plZr6IBRpBMJGFoSlGO5bM=\r\n=F7Ii\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"kyeotic","email":"tim@kye.dev"},"directories":{},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/dynamo-arc_2.2.1_1649977830442_0.7852082491922281"},"_hasShrinkwrap":false}},"time":{"created":"2020-09-03T02:28:02.201Z","1.0.0":"2020-09-03T02:28:02.353Z","modified":"2022-04-30T17:33:55.242Z","1.1.0":"2020-09-09T16:53:07.965Z","1.2.0":"2020-09-09T19:54:35.130Z","1.3.0":"2020-09-09T22:17:56.456Z","1.3.1":"2020-09-09T22:47:02.872Z","1.3.2":"2020-10-29T20:43:51.626Z","1.3.3":"2021-01-10T20:48:57.621Z","1.4.0":"2021-01-21T17:42:48.985Z","1.5.0":"2021-01-21T18:29:47.336Z","1.5.1":"2021-02-16T17:51:36.104Z","2.0.0-0":"2021-02-17T22:23:40.794Z","2.0.0-1":"2021-02-18T00:19:39.550Z","2.0.0-2":"2021-02-18T06:18:48.484Z","2.0.0-3":"2021-02-18T21:46:45.703Z","2.0.0-4":"2021-02-19T00:10:45.992Z","2.0.0-5":"2021-02-19T00:50:24.638Z","2.0.0-6":"2021-02-19T04:34:07.923Z","2.0.0-7":"2021-02-19T21:29:58.544Z","2.0.0-8":"2021-02-22T22:08:42.942Z","2.0.0-9":"2021-02-24T02:41:06.606Z","1.6.0":"2021-08-03T15:51:30.394Z","1.7.0":"2021-08-11T17:27:38.263Z","2.0.0-10":"2021-09-08T16:02:32.806Z","2.0.0-11":"2021-09-09T22:24:24.768Z","2.0.0-12":"2021-09-14T16:32:16.392Z","2.0.0-13":"2021-09-15T21:20:06.544Z","2.0.0-14":"2021-09-16T20:00:25.581Z","2.0.0":"2021-10-15T15:53:30.339Z","2.1.0":"2021-10-18T19:53:06.253Z","2.2.0-0":"2021-11-12T04:32:02.697Z","2.2.0-1":"2021-11-12T06:38:54.301Z","2.2.0":"2021-11-12T16:26:45.096Z","2.2.1":"2022-04-14T23:10:30.593Z"},"maintainers":[{"name":"kyeotic","email":"tim@kye.dev"}],"description":"A dynamo data client designed for use with DyanmoDB Single Table applications","keywords":["dynamo"],"repository":{"type":"git","url":"git+ssh://git@github.com/Nike-Inc/dynamo-arc.git"},"author":{"name":"Tim Kye"},"license":"Apache-2.0","readme":"# Dynamo Arc\n\nA dynamo data client designed for use with DyanmoDB Single Table applications.\n\n## Quick Start\n\n```javascript\nconst { makeClient, Store, Cache } = require('dynamo-arc')\n\n// Setup the base client\nconst dynamo = makeClient({\n  tableName: 'my-datastore',\n  clientConfig: { region: 'us-west-2' }\n})\n\n// Define a type-store\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'recordId', dynamo })\n  }\n}\nconst recordStore = new RecordStore({ dynamo })\nconst record = await recordStore.get('1')\nrecord.name = 'primary'\nrecord.age = 300\nrecord.scopes = [{ name: 'top', isActive: true}, { name: 'left', isActive: false }]\nawait recordStore.put(record)\n\n// Setup generic cache\nconst cache =  new Cache({ dynamo: context.dynamo })\nconst externalRecord = cache.get(\n  'a',\n  async () => externalService.get('a'),\n  { ttl: 20000 }\n)\n```\n\n## The Basics\n\n**Dynamo Arc** provides a simple API for interacting with a DynamoDB table that stores multiple schemas, which we call the **Single Table Pattern**. An incredible presentation of this method is given in this [AWS RE:invent talk](https://www.youtube.com/watch?v=jzeKPKpucS0). If you are not familiar with how to use a single table to store multiple data schemas it is strongly recommended that you watch the video, it will greatly increase the chances that you use this library correctly.\n\nTo quickly summarize: when using this library it is assumed your entire application uses a single DynamoDB table with generic partition keys, with optional range keys, that use a composite form to identify the record. For example `_PROJECT_:abcd` would identify a record of the **project** type whose ID was `abcd` and `_USER_:3243` would identify a record of the **user** type whose ID was `3243`. The actual data for the object is stored in a generic key, in this case `data`, which is a **DynamoDB Map**. This allows any number of types to occupy the same table, using a generic table-level schema, which comes with a ridiculous list of benefits at the minor cost of complexity that it takes to understand the composite keys.\n\nThis library provides a simple, async-friendly API for interacting with such a table. Interactions at the store level will be with plain JS object; all the complexity of composite key handling are abstracted.\n\n## Concepts\n\n**The dynamo client**: using this library requires constructing a special DynamoDB client using the exported **makeClient** function, which is provided to the various **stores** that are defined for each record/schema type. The examples throughout this documentation refer to this object as the *dynamo client*, while the code uses the variable `dynamo`.\n\n**stores**: each record type will have a dedicated store used to handle the composite key logic necessary for packing and unpacking. These are defined by extending the exported `Store` class and provided a `type`, along with optional field-mapping for `idKey` and `sortKey` properties to extract from the record.\n\n**cache**: the exported `Cache` class is designed to be used once-per-app to construct a generic **ttl cache**. Its basic use is shown above in the **Quick Start** section, with a unique *key*, a *cache-miss function* that fetches the item if it is missing or expired in the cache, and optional *ttl config*. While it might be surprising to overload your primary datastore as a cache, when properly re-using connections DynamoDB can achieve single-digit millisecond response (even in Node) making it a fast, easy to use caching layer.\n\n## Configuration\n\nThe configuration for all exported functions/classes can be found below.\n\n### Dynamo Client\n\n```typescript\nfunction makeClient({\n  tableName,\n  idField = 'id', // partition key of the table\n  sortField = 'sort_key', // sort key of the table\n  typeIndex = 'type-index', // index used for the TYPE value\n  ttlField = 'ttl', // ttl field of the table (necessary for the Cache)\n  hasTtlField = true,\n  hasSortField = true,\n  clientConfig,\n  translateConfig,\n  dynamoConfig\n}: {\n  tableName: string\n  idField?: string\n  sortField?: string\n  typeIndex?: string\n  ttlField?: string\n  hasTtlField?: boolean\n  hasSortField?: boolean\n  clientConfig?: DynamoDBClientConfig\n  translateConfig?: TranslateConfig\n}, client?: DynamoDBClient // must provide either client param or clientConfig)\n): ArcDynamoClient {}\n```\n\n`makeClient` returns a modified dynamo client, typed as `ArcDynamoClient`, that tracks additional data about the table such as its name, various fields, and features. It can either be passed an existing `DynamoDBClient` as via its second parameter, or it can create one using the `clientConfig` option in the first parameter. These options are mutually exclusive, and one of them is required.\n\nThe only required property for the first parameter is the `tableName`, which is the full name of the Dynamo table. The other fields are optional with default values.\n\nThe `translateConfig` configures the DynamoDBDocument client's marshalling options, though Arc uses different default values. The library exports `ArcTranslateDefaults` and `AwsTranslateDefaults`, though a custom configuration can be supplied.\n\n## Store\n\n```typescript\ndeclare class Store <T>{\n  public readonly [_type]: string\n  public readonly [_dynamo]: ArcDynamoClient\n  public readonly [_logger]: Logger\n  public readonly [_idKey]: keyof T & string\n  public readonly [_sortKey]?: (keyof T & string) | undefined\n  public readonly [_delimiter]: string\n\n  constructor(\n    dynamo: ArcClient,\n    logger: Logger, // see Logging section below\n    type: string,\n    idKey = 'id',\n    sortKey?: string,\n    delimiter = ':'\n  )\n}\n```\n\nThe only required properties for the `Store` are the `dynamo` client, which must be the result of the `makeClient` function, the `type`, which is used to create the composite key for the record, and the `idKey` which is used to extract the primary key from the type.\n\nThe simplest child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({ type: '_RECORDS_', idKey: 'id', dynamo })\n  }\n```\n\nA fully configured child class\n\n```javascript\nclass RecordStore extends Store {\n  constructor({ dynamo }) {\n    super({\n      dynamo,\n      type: '_ORDER_ITEM_',\n      idKey: 'orderId',\n      sortKey: 'itemId',\n      delimiter: '::',\n      logger: console\n    })\n  }\n```\n\n## API\n\n```typescript\ninterface TableKey {\n    // The properties on a Store's Key are determined\n    // by its configuration.\n    // It will have an idKey, and optionally a sortKey\n    [key: string]: string\n}\n\n// Raw Item from Dynamo\ninterface DynamoRecord {}\n\n// Stand in for the normal DocumentClient params for the given function\n// The TableName property is automatically filled in by Arc\ninterface DynamoParams {}\n\n// Stand in for the normal DocumentClient result for the given function\ninterface DynamoResult {}\n\ninterface Store<T> {\n  getTableName(): string\n  \n  /** Join id segments together with the configured delimiter */\n  join(...idSegments: string[]): string\n  \n  /** Create the ID field of this type by joining it to the store's configured TYPE  */\n  typeKey(...idSegments: string[]): string\n  \n  /** Creates the Key object used by dynamo. Includes a sort key if configured on this store */\n  asKey(id:string, sortKey?: string): StoreKey\n  \n  /** Convert the DynamoDB record back into the originally stored JS object */\n  fromDb(item: DynamoRecord): T\n\n  /** Convert a plain JS object into a DynamoDB record */\n  toDb(item: T): DynamoRecord\n\n  /** Get a keyed item from Dynamo */\n  async get(id:string, sortKey?:string): Promise<T>\n\n  /** Delete the item from Dynamo matching the provided key */\n  async delete(id:string, sortKey:string): Promise<void>\n\n  /** Delete all items */\n  async deleteAll(items: T[]): Promise<void>\n\n  /** Create or Update the item in Dynamo */\n  async put(item: T): Promise<T>\n\n  /** Put all items */\n  async putAll(items: T[]): Promise<void>\n\n  /** Execute a query against the configured Dynamo table */\n  async query(params: DynamoParams): Promise<DynamoResult>\n  \n  /** Execute a scan against the configured Dynamo table */\n  async scan(params: DynamoParams): Promise<DynamoResult>\n\n  /** Execute a batchGet against the configured Dynamo table */\n  async batchGet(keys: StoreKey[]): Promise<DynamoResult>\n\n  /** Execute a batchWrite against the configured Dynamo table */\n  async batchWrite(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute a query against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async queryAll(params: DynamoParams): Promise<T[]>\n  \n  /** Execute a scan against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async scanAll(params: DynamoParams): Promise<T[]>\n\n  /** Execute a batchGet against the configured Dynamo table with automatic paging, mapped through fromDb() */\n  async batchGetAll(keys: StoreKey[]): Promise<T[]>\n\n  /** Execute a batchWrite against the configured Dynamo table with automatic paging */\n  async batchWriteAll(changes: (StoreKey | T)): Promise<DynamoResult>\n\n  /** Execute an automatically paged query by processing one page at a time */\n  async queryByPage(\n    params: QueryAllInput,\n    /** Async function that receives an array of items from the current page. If it resolves `false` paging will stop  */\n    pageFn: (page: T[]) => Promise<void | boolean>\n  ): Promise<void>\n\n  /** Execute an automatically paged query against the typeIndex for the type configured on this store */\n  getAll(): Promise<T[]>\n}\n```\n\n## Cache\n\nThe cache takes a `dynamo` object and returns a store that uses dynamo as a caching layer by handling various `ttl` values.\n\n\n### Setup\n```javascript \nconst { Cache } = require('dynamo-arc')\nreturn new Cache({ dynamo: dynamo })\nconst getter = () => cache.get(\n  'some-id',\n  () => someExpensiveOp(),\n  { staleAfter: 10000 }\n)\nconst freshValue = await getter()\nconst cachedValue = await getter()\n```\n\n### API\n\n```typescript\ninterface CacheOptions {\n    permanent?: boolean\n    ttl?: number\n    staleAfter?: number\n}\n\ninterface CacheKey extends CacheOptions {\n    id: string\n}\n\ninterface Cache {\n  get<T>(key: string, cacheMissFn: () => Promise<T>, options?: CacheOptions): Promise<T>\n  set<T>(key: string, value: T, options?: CacheOptions): Promise<T>\n  // This takes an array of object with an ID and CacheOptions\n  // It will return the first object from the cache whose ID matches one in the array\n  // Or it will call the cacheMissFn and write the result to every ID in the array\n  batchGet<T>(keys: CacheKey[], cacheMissFn: () => Promise<T>): Promise<T>\n}\n```\n\n## fromDb()/toDb()\n\nWorking with a single table means overloading the schema. Since every type is using well-known properties for `id` and `sort_key` and the various GSIs the rest of the data needs to go into a collision resistant property: `data`. When writing an object with `put` the object is sent to dynamo after casting through `toDb(item)`.\n\n```javascript\ntoDb(item) {\n  let id = item[this[_idKey]]\n  let data = { ...item }\n\n  const dbItem = {\n    ...this.asKey(id, item[this[_sortKey]]),\n    type: this[_type],\n    // This is to make it easier to find in the dynamo console\n    typeId: id,\n    // datetime props\n    createdOn: item.createdOn,\n    updatedOn: Date.now(),\n    //\n    data, // <--- where the actual object is stored!!\n    //\n  }\n\n  return dbItem\n}\n```\n\nWhen reading with `get`, `queryAll`, `scanAll`, or `batchGetAll` the raw response from dynamo needs to have the `data` property unpacked. Extraction is much simpler, so this is the entire default `fromDb(item)` function.\n\n```javascript\nfromDb(item) {\n  if (!item || !item.data) return null\n  item = item.data\n  return item\n}\n```\n\nBoth of these functions are defined on the `Store`, so they can be overriden as necessary. The most common use case for this is overriding `toDb` in order to add GSI indexing properties\n\n```javascript\n// Class Method on an \"extends Store\" class\ntoDb(item) {\n  return {\n    ...super.toDb(item),\n    // custom owner index\n    gsi1_key: this.typeKey(item.ownerId), \n    gsi1_sort: item.id\n  }\n}\n```\n\n> Note: because the `query`, `scan`, `batchWrite` and `batchGet` methods do not automatically page they return the raw dynamo response so that the caller can access the paging properties. This means their responses **are not run through `fromDb()` first!**\n\n## Querying GSIs\n\nGetting data out of a GSI is easy as long as the GSI key uses the `this.typeKey()` as seen above, which ensure the store's configured *type* is combined with the intended ID. Doing the same on the query filters the query so that only records of the correct type are read from the GSI, despite the Single Table's GSI containing records of many types\n\n```javascript\n// Class Method on an \"extends Store\" class\nasync getByOwnerId(ownerId) {\n  return this.queryAll({\n    IndexName: 'gsi1-index',\n    ScanIndexForward: false,\n    KeyConditionExpression: '#ownerId = :ownerId',\n    ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n    ExpressionAttributeValues: { ':ownerId': this.typeKey(ownerId) }\n  })\n}\n```\n## Stream processing queries\n\nthe `store.queryByPage` function provides streaming access to the results of a query. Since only one page is brought into memory at a time this can allow queries to be processed that might otherwise cause the process to consume more memory than its host can provide.\n\n```typescript\n /** Execute an automatically paged query by processing one page at a time */\n  queryByPage(params: DynamoParams, pageFn: T[]) => any)\n```\n\nThe `pageFn` is passed an array of `fromDb()` mapped rows and its result is awaited. The paging process can be halted early by returning the `storeSymbols.pageBreak` symbol from the `pageFn`.\n\n**forEachPage** is an older, deprecated method that functions similar to `queryByPage`, but uses a pre-canned query that pages the entire `typeIndex` for the current store. This behavior (and more!) is possible with `queryByPage`.\n\n## Modeling Relationships\n\nIn Graph Theory an [edge](https://en.wikipedia.org/wiki/Glossary_of_graph_theory#edge) is a relationship between two nodes. Since DynamoDB is a NoSQL store there are no native relationships, but they can be simulated by creating records that use the _primary_ and _sort_ keys on the table. These records are called _edges_. Arc has tools for managing _edges_ modelling either parent-child relationships (one-to-many) or associative relationships (many-to-many).\n\nThere are two classes for working with _edges_.\n\n* `EdgeStore` - Used for managing edges in an associative relationship\n* `BaseEdgeStore` - Used for managing edges in an associative relationship. This Class is intended to provide additional flexibility for cases when the safety checks or automatic edge-selection on the `EdgeStore` or `ChildStore` are too restrictive. When possible prefer the `EdgeStore` or `ChildStore`.\n\n### How it works\n\nHere is an example of a parent child relationship as it would appear in Dynamo\n\n```javascript\nconst node = { id: '_USER_:4332' }\nconst edge = { id: '_USER_PREF_:4332', sort_key: 'volume', /* ... */}\n```\n\nNotice how in the edge-record the id is the same as the User ID. This allows a query to select all of the _user preference relationship_ by knowing only the User ID and the type (`_USER_PREF_`).\n\nHere is an example of a two-way relationship\n\n```javascript\nconst user = { id: '_USER_:4332' }\nconst team = { id: '_TEAM_:8867' }\nconst edge = {\n  id: '_USER_TEAM_MEMBER_:4332',\n  sort_key: '8867',\n  gsi1_key: '_USER_TEAM_MEMBER_:8867',\n  gsi1_sort: '4332' \n  /* ... */\n}\n```\n\nHere, again, a relationship to one node is stored on the primary key. A secondary relationship is modeled on a GSI, using the same method. This allows a two-way, or \"many-to-many\", relationship to be modeled.\n\n### Child Relationships\n\nModeling child relationships can be done using the standard store and a Global Secondary Index (GSI).\n\nA common one-to-many relationship is a set of object with a single \"owner\".\n\n```ts\n\nimport { Store, StoreSubConfig } from 'dynamo-arc'\n\nclass ProjectStore extends Store<Project> {\n  constructor({ dynamo }: StoreSubConfig<Project>) {\n    super({\n      dynamo,\n      type: '_PROJECT_',\n      idKey: 'id',\n      sortKey: 'ownerId',\n    })\n  }\n\n  async getByOwnerId(ownerId) {\n    return this.queryAll({\n      IndexName: 'gsi1-index',\n      ScanIndexForward: false,\n      KeyConditionExpression: '#ownerId = :ownerId',\n      ExpressionAttributeNames: { '#ownerId': 'gsi1_key' },\n      ExpressionAttributeValues: { ':ownerId': this.typeKey(item.ownerId) }\n    })\n  }\n\n  toDb(item) {\n    return {\n      ...super.toDb(item),\n      // custom owner index used by getByOwnerId\n      gsi1_key: this.typeKey(item.ownerId), \n      gsi1_sort: item.id\n    }\n  }\n}\n\nconst projectStore = new ProjectStore({ dynamo })\n\nconst projectA = { id: 'a', ownerId: 'X' }\nconst projectB = { id: 'b', ownerId: 'X' }\n\nawait projectStore.put(projectA)\nawait projectStore.put(projectB)\n\nconst [a, b] = await projectStore.getByOwnerId('X')\n\n```\n\nThe above example demonstrates everything needed for a parent-child (one-to-many) relationship between an owner and a project. Since DynamoDB can have up to 20 GSIs per total (previously 5) each type can have 20 unique parent-child relationships modeled.\n\n### Associate Relationships with `EdgeStore`\n\nThe `EdgeStore` simplifies working with many-to-many relationships between two types. It requires the `dynamo` client to be configured with a `sortField`. It must also be provided a `secondaryIndex` in its constructor to use as a GSI for selecting secondary edges.\n\n**Associate Relationship Configuration**\n\n```typescript\nimport { EdgeStore, EdgeStoreSubConfig } from 'dyanamo-arc'\n\ninterface User {\n  id: string\n  teams: Team[]\n}\n\ninterface Team {\n  id: string\n  members: User[]\n}\n\ninterface UserTeam {\n  userId: string\n  teamId: string\n  role: string\n}\n\nclass UserTeamStore extends EdgeStore<UserTeam> {\n  // The EdgeStoreSubConfig simplifies sub-class configs by omitting\n  // properties that are expected to be hard-coded, such as the strings below\n  // This allows new UserTeamStore() to safely provide only \"dynamo\"\n  constructor({ dynamo }: EdgeStoreSubConfig<UserTeam>) {\n    super({\n      dynamo,\n      type: '_EGDE_',\n      idKey: 'userId',\n      sortKey: 'teamId',\n      secondaryIndex: 'gsi1-index',\n    })\n  }\n\n  // The EdgeStore makes its type-generic methods \"protected\"\n  // in order to force sub-classes to provide type-specific names\n  // Users of the UserTeamStore shouldn't have to track which of \"user\" or \"team\"\n  // is the \"primary\" or \"secondary\" nodes, they should be given clear names\n  async getTeamsForUser(userId: string): Promise<UserTeam>[] {\n    return this.getEdgesByPrimaryId(userId)\n  }\n\n  async getUsersOnTeam(teamId: string): Promise<UserTeam>[] {\n    return this.getEdgesBySecondaryId(teamId)\n  }\n}\n\nconst userTeamStore = new UserTeamStore({ dynamo })\n\nconst teamsForUserX = await userTeamStore.getTeamsForUser('X')\nconst usersOnTeamA = await userTeamStore.getUsersOnTeam('a')\n\n// add user X to team b\nawait userTeamStore.put({ userId: 'X', teamId: 'b', role: 'member' })\n\n// remove user X from team b\nawait userTeamStore.delete('X', 'b')\n\n// force team b to have the following users\nawait userTeamStore.syncEdgesBySecondary('b', [\n  { userId: 'X', teamId: 'b', role: 'member' },\n  { userId: 'Y', teamId: 'b', role: 'member' },\n])\n```\n\n> The `EdgeStoreSubConfig` is a utility type that simplifies the config of `EdgeStore` sub-classes by removing the properties that sub-class constructor would hard-code for the `EdgeStore`\n\n### BaseEdgeStore\n\nThe `BaseEdgeStore` extends the `Store` with a pair of methods that assist with creating a `batchWriteAll` call to add and remove records. It is used by the `EdgeStore` to implement the `syncEdgesBy[Primary|Secondary]` and `getEdgesBy[Primary|Secondary]Id` functions. If the `EdgeStore` has an implementation that does not work with your model you can implement your own Edge Store on top of the `BaseEdgeStore`.\n\nThe `BaseEdgeStore` implements the following\n\n* `async syncEdges(dbEdges: Edge[], edges: Edge[]): Promise<[Edge[], Edge[]]>`: Filter the provided edges and execute a `batchWriteAll` against Dynamo.\n* `protected filterEdges(leftEdges: Edge[], rightEdges: Edge[]): Edge[]`: Used to filter edges in the left that are missing from the right. The default implementation compares the keys defined for the store.\n\n","readmeFilename":"README.md","homepage":"https://github.com/Nike-Inc/dynamo-arc#readme","bugs":{"url":"https://github.com/Nike-Inc/dynamo-arc/issues"}}