{"_id":"@asabytes/dynamodb","_rev":"2-2dc0cbf01c89a67948188d12234d8ce7","name":"@asabytes/dynamodb","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@asabytes/dynamodb","version":"1.0.0","keywords":["datamapper","dynamodb","aws","amazon","nosql","zod","typescript"],"author":{"name":"Salvatore Agri"},"license":"MIT","_id":"@asabytes/dynamodb@1.0.0","maintainers":[{"name":"asabytes","email":"salvatore.agri@gmail.com"}],"homepage":"https://github.com/asa75/dynats#readme","bugs":{"url":"https://github.com/asa75/dynats/issues"},"dist":{"shasum":"7df41b1ea46fa63a7d3a87a763ac1eb2efb63da4","tarball":"https://registry.npmjs.org/@asabytes/dynamodb/-/dynamodb-1.0.0.tgz","fileCount":9,"integrity":"sha512-OPF/P/yDTe/OGa76m614rWwX+RSZlAEz4uWcd4DDGGMk4iUtdfm2XTlRPTrRc4p9tGc/fDrtNpuIB/Zm1Dh4jw==","signatures":[{"sig":"MEYCIQC1tHcm8FpyhzJo4YcglWKD4mmbE0O2X+3Kc+2vawdf5gIhANuvPKJV+/wlKYtZYjsWj/uPdf3DFfZFCO4frFr3PrKe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":604169},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"73ea78e5f2a2c9c679a93b275e973acd7889003f","scripts":{"test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup","typecheck":"tsc --noEmit","test:watch":"node --experimental-vm-modules node_modules/jest/bin/jest.js --watch","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"asabytes","email":"salvatore.agri@gmail.com"},"overrides":{"esbuild":"^0.28.1","js-yaml":"^4.2.0"},"repository":{"url":"git+https://github.com/asa75/dynats.git","type":"git"},"_npmVersion":"11.13.0","description":"DynamoDB data mapper for Node.js, in TypeScript, with Zod schemas and the AWS SDK v3","directories":{},"sideEffects":false,"_nodeVersion":"24.17.0","dependencies":{"uuid":"^11.0.0","@aws-sdk/lib-dynamodb":"^3.700.0","@aws-sdk/client-dynamodb":"^3.700.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^4.0.0","jest":"^29.7.0","tsup":"^8.0.0","ts-jest":"^29.2.0","typescript":"^5.6.0","@types/jest":"^29.5.0","@types/node":"^22.0.0","@jest/globals":"^29.7.0"},"peerDependencies":{"zod":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/dynamodb_1.0.0_1782891711762_0.6972175823094677","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@asabytes/dynamodb","version":"1.0.1","publishConfig":{"access":"public"},"description":"DynamoDB data mapper for Node.js, in TypeScript, with Zod schemas and the AWS SDK v3","license":"MIT","author":{"name":"Salvatore Agri"},"repository":{"type":"git","url":"git+https://github.com/asa75/dynamodb.git"},"bugs":{"url":"https://github.com/asa75/dynamodb/issues"},"homepage":"https://github.com/asa75/dynamodb#readme","type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"engines":{"node":">=18.0.0"},"keywords":["datamapper","dynamodb","aws","amazon","nosql","zod","typescript"],"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","test:watch":"node --experimental-vm-modules node_modules/jest/bin/jest.js --watch","prepublishOnly":"npm run typecheck && npm test && npm run build"},"overrides":{"esbuild":"^0.28.1","js-yaml":"^4.2.0"},"peerDependencies":{"zod":"^4.0.0"},"dependencies":{"@aws-sdk/client-dynamodb":"^3.700.0","@aws-sdk/lib-dynamodb":"^3.700.0","uuid":"^11.0.0"},"devDependencies":{"@jest/globals":"^29.7.0","@types/jest":"^29.5.0","@types/node":"^22.0.0","jest":"^29.7.0","ts-jest":"^29.2.0","tsup":"^8.0.0","typescript":"^5.6.0","zod":"^4.0.0"},"gitHead":"122bbe6784ef3357d5f9b7fe65fb6e4182cc92ab","_id":"@asabytes/dynamodb@1.0.1","_nodeVersion":"24.17.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-fz47Emv2cHLRpnoy9wynfd/jxAU+eUd/veV0O/tIhxDyyrHd2ZPR0nn8T0qADknpEgUFdjzPhAgvVnHDenvg3g==","shasum":"0ac59bf0b26725c261ec0cc0756fe9d3a07246df","tarball":"https://registry.npmjs.org/@asabytes/dynamodb/-/dynamodb-1.0.1.tgz","fileCount":9,"unpackedSize":604175,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCic84GJrA1G2ehRalQSBPvxJsRCvQ2oX/raRWiVLOpVAIhAJWxnrYlUNFtXLdQt+3+WrjyUwGHdh5kuQs/DeKW6sWL"}]},"_npmUser":{"name":"asabytes","email":"salvatore.agri@gmail.com"},"directories":{},"maintainers":[{"name":"asabytes","email":"salvatore.agri@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dynamodb_1.0.1_1782893035908_0.4780761996365219"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-01T07:41:51.629Z","modified":"2026-07-01T08:03:56.198Z","1.0.0":"2026-07-01T07:41:51.919Z","1.0.1":"2026-07-01T08:03:56.081Z"},"bugs":{"url":"https://github.com/asa75/dynamodb/issues"},"author":{"name":"Salvatore Agri"},"license":"MIT","homepage":"https://github.com/asa75/dynamodb#readme","keywords":["datamapper","dynamodb","aws","amazon","nosql","zod","typescript"],"repository":{"type":"git","url":"git+https://github.com/asa75/dynamodb.git"},"description":"DynamoDB data mapper for Node.js, in TypeScript, with Zod schemas and the AWS SDK v3","maintainers":[{"name":"asabytes","email":"salvatore.agri@gmail.com"}],"readme":"# @asabytes/dynamodb\n\nA DynamoDB data mapper for Node.js — inspired by and a TypeScript port of the\n[`dynamodb`](https://www.npmjs.com/package/dynamodb) package\n([baseprime/dynamodb](https://github.com/baseprime/dynamodb)), modernized to use:\n\n- **[Zod 4](https://zod.dev)** for schemas instead of Joi\n- the **[AWS SDK v3](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/welcome.html)**\n  (`@aws-sdk/client-dynamodb` + `@aws-sdk/lib-dynamodb`)\n- **Promises / async-await** everywhere instead of callbacks\n- **async iterators** for streaming instead of Node `Readable` streams\n\nThe data-modeling logic — schemas, serialization, query/scan builders, update\nexpressions, secondary indexes, batch get, parallel scan, hooks — is a faithful\nport of the original library.\n\n## Installation\n\n```bash\nnpm install @asabytes/dynamodb zod\n```\n\n`@aws-sdk/client-dynamodb`, `@aws-sdk/lib-dynamodb`, and `uuid` are runtime\ndependencies and are installed automatically. `zod` is a **peer dependency** —\ninstall it alongside (npm 7+ adds it for you) so your app and this library share\na single Zod instance.\n\n## Getting started\n\nThe SDK v3 reads credentials and region from the standard provider chain\n(environment, shared config, IAM role, etc.). To use a custom client:\n\n```ts\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport dynamo from '@asabytes/dynamodb';\n\ndynamo.dynamoDriver(new DynamoDBClient({ region: 'us-east-1' }));\n```\n\n## Define a model\n\nUse `z` (re-exported from this package) for ordinary attributes and\n`dynamo.types` for the DynamoDB-specific helpers (sets, uuid, binary).\n\n```ts\nimport dynamo, { z } from '@asabytes/dynamodb';\n\nconst Account = dynamo.define('Account', {\n  hashKey: 'email',\n  timestamps: true, // adds createdAt / updatedAt\n  schema: {\n    email: z.string().email(),\n    name: z.string(),\n    age: z.number().optional(),\n    roles: dynamo.types.stringSet(),\n    settings: z.object({\n      nickname: z.string().optional(),\n      acceptedTerms: z.boolean().default(false),\n    }),\n  },\n});\n\nconst BlogPost = dynamo.define('BlogPost', {\n  hashKey: 'email',\n  rangeKey: 'title',\n  schema: {\n    email: z.string().email(),\n    title: z.string(),\n    content: dynamo.types.binary(),\n    tags: dynamo.types.stringSet(),\n  },\n});\n```\n\n### Type inference\n\nModels are generic over their schema: `define` infers the item type (via\n`z.infer`) and the hash/range key value types, so reads, writes, queries, and\nkey arguments are all statically typed — no manual type parameters needed.\n\n```ts\nconst Account = dynamo.define('Account', {\n  hashKey: 'email',\n  schema: { email: z.string(), age: z.number().optional() },\n});\n\nconst acc = await Account.get('a@b.com'); // hash key must be a string\nacc?.get('age');                          // number | undefined\nawait Account.get(123);                    // ✖ compile error: email is a string\n\nfor await (const a of Account.scan().items()) {\n  a.get('email'); // string\n}\n\nAccount.scan().where('age').gte(21); // ✔\nAccount.scan().where('nope');        // ✖ compile error: not an attribute\n```\n\n> Note: date attributes (`z.coerce.date()`) infer as `Date`, but DynamoDB stores\n> and returns them as ISO strings — values read back are strings at runtime.\n\n### Schema types\n\n`dynamo.types` provides the helpers that have no direct Zod primitive:\n\n| Helper | DynamoDB type |\n| --- | --- |\n| `dynamo.types.stringSet()` | String Set (`SS`) |\n| `dynamo.types.numberSet()` | Number Set (`NS`) |\n| `dynamo.types.binarySet()` | Binary Set (`BS`) |\n| `dynamo.types.binary()` | Binary (`B`) |\n| `dynamo.types.uuid()` | String, defaults to a generated UUID v4 |\n| `dynamo.types.timeUUID()` | String, defaults to a generated UUID v1 |\n\nEverything else is a plain Zod schema: `z.string()`, `z.number()`,\n`z.boolean()`, `z.coerce.date()`, `z.object({...})`, `z.array(...)`, etc.\n\n> **Unknown keys.** A plain `schema: { ... }` record compiles to a *strict*\n> object — unknown attributes are rejected, matching Joi's default. To allow\n> dynamic attributes, pass a loose Zod object instead:\n> `schema: z.looseObject({ id: z.string() })` (the equivalent of Joi `.unknown()`).\n\n## How keys are stored\n\nA hash key, range key, or secondary-index key is **not** stored specially — it is\njust a normal attribute stored under the exact name you declare. What makes it a\n\"key\" is the table's `KeySchema`, which references those attribute names. A\nsecondary index adds no new attributes; it designates existing ones as that\nindex's keys (so an item only appears in a sparse GSI when it has those attrs).\n\n```ts\nconst GameScore = dynamo.define('GameScore', {\n  hashKey: 'userId',      // partition key attribute name\n  rangeKey: 'gameTitle',  // sort key attribute name\n  schema: { userId: z.string(), gameTitle: z.string(), topScore: z.number() },\n  indexes: [{ hashKey: 'gameTitle', rangeKey: 'topScore', name: 'GameTitleIndex', type: 'global' }],\n});\n```\n\nInspect the key names at runtime from the compiled schema, or the live table:\n\n```ts\nGameScore.table.schema.hashKey         // 'userId'\nGameScore.table.schema.rangeKey        // 'gameTitle'\nGameScore.table.schema.globalIndexes   // { GameTitleIndex: { hashKey: 'gameTitle', rangeKey: 'topScore', ... } }\nGameScore.table.schema.secondaryIndexes // local (LSI) indexes, keyed by name\n\nconst { Table } = await GameScore.describeTable();\nTable.KeySchema              // [{ AttributeName: 'userId', KeyType: 'HASH' }, { AttributeName: 'gameTitle', KeyType: 'RANGE' }]\nTable.AttributeDefinitions   // only key attributes are declared, with their type\nTable.GlobalSecondaryIndexes // [{ IndexName: 'GameTitleIndex', KeySchema: [...] }]\n```\n\nKey values are ordinary attribute values. This library hands you native JS (the\nDocumentClient marshals to/from the low-level form), but on the wire they are:\n\n| Schema type | Key `AttributeType` | Low-level value | Read back as |\n| --- | --- | --- | --- |\n| `z.string()` | `S` | `{ \"S\": \"u1\" }` | `string` |\n| `z.number()` | `N` | `{ \"N\": \"4200\" }` | `number` |\n| `dynamo.types.binary()` | `B` | `{ \"B\": <bytes> }` | `Uint8Array` |\n| `z.coerce.date()` | `S` | `{ \"S\": \"2026-06-23T…Z\" }` | ISO `string` |\n\nKey attributes (including GSI/LSI keys) must be `S`, `N`, or `B` — DynamoDB does\nnot allow boolean/object/set key types.\n\n## Create tables\n\n```ts\nawait dynamo.createTables({\n  BlogPost: { readCapacity: 5, writeCapacity: 10 },\n  Account: { readCapacity: 20, writeCapacity: 4 },\n});\n\nawait BlogPost.deleteTable();\n```\n\n## CRUD\n\n```ts\n// create (single or array)\nconst acc = await Account.create({ email: 'foo@example.com', name: 'Foo', age: 21 });\nawait Account.create([{ email: 'a@x.com' }, { email: 'b@x.com' }]);\n\n// conditional create\nawait Account.create({ email: 'foo@example.com' }, { overwrite: false });\n\n// get by key (scalar, hash+range, or key object)\nconst a = await Account.get('foo@example.com');\nconst p = await BlogPost.get('foo@example.com', 'Hello World');\nconst p2 = await BlogPost.get({ email: 'foo@example.com', title: 'Hello World' });\nconst consistent = await Account.get('foo@example.com', { ConsistentRead: true });\n\n// update (null removes an attribute; $add / $del mutate numbers and sets)\nawait Account.update({ email: 'foo@example.com', name: 'Bar' });\nawait Account.update({ email: 'foo@example.com', age: { $add: 1 } });\nawait BlogPost.update({ email: 'foo@example.com', title: 'Hello World', tags: { $del: 'cloud' } });\n\n// conditional update\nawait Account.update({ email: 'foo@example.com', name: 'Bar' }, { expected: { age: 22 } });\n\n// destroy\nawait Account.destroy('foo@example.com');\nawait BlogPost.destroy({ email: 'foo@example.com', title: 'Hello World' });\n```\n\nInstances expose `save()`, `update()`, `destroy()`, `get(key)`, `set(attrs)`,\nand `toJSON()`:\n\n```ts\nconst acc = new Account({ email: 'test@example.com', name: 'Test' });\nawait acc.save();\nacc.set({ age: 22 });\nawait acc.update();\n```\n\n## Query\n\n```ts\nconst result = await BlogPost.query('werner@example.com')\n  .where('title').beginsWith('Expanding')\n  .filter('tags').contains('cloud')\n  .attributes(['title', 'content'])\n  .limit(10)\n  .descending()\n  .exec();\n\nconsole.log(result.Items, result.Count);\n\n// load every page\nconst all = await BlogPost.query('werner@example.com').loadAll().exec();\n\n// against a global secondary index\nawait GameScore.query('Galaxy Invaders').usingIndex('GameTitleIndex').descending().exec();\n```\n\nKey conditions: `equals`/`eq`, `lt`, `lte`, `gt`, `gte`, `beginsWith`,\n`between`. Filters add `ne`, `null`, `exists`, `contains`, `notContains`, `in`.\n\n## Scan\n\n```ts\nawait Account.scan().where('age').gte(21).exec();\nawait Account.scan().loadAll().exec();\nawait Account.scan().where('age').gte(21).select('COUNT').exec();\n```\n\n## Streaming with async iterators\n\n`query`, `scan`, and `parallelScan` are async-iterable. Pagination past the\nfirst page is gated on `loadAll()` (matching the original streaming behaviour).\n\n```ts\n// iterate page by page\nfor await (const page of Account.scan().loadAll()) {\n  console.log(page.Items.length);\n}\n\n// iterate item by item\nfor await (const acc of Account.scan().loadAll().items()) {\n  console.log(acc.get('email'));\n}\n```\n\n## Parallel scan\n\n```ts\nconst result = await Account.parallelScan(8).where('age').gte(18).exec();\n\nfor await (const page of Account.parallelScan(4)) {\n  console.log('segment page', page.Items.length);\n}\n```\n\n## Batch get\n\n```ts\nconst accounts = await Account.getItems(['a@x.com', 'b@x.com', 'c@x.com']);\n\nconst posts = await BlogPost.getItems([\n  { email: 'a@x.com', title: 'Hello' },\n  { email: 'a@x.com', title: 'World' },\n], { ConsistentRead: true });\n```\n\n## Single-table design\n\n`defineSingleTable` is a thin layer over `dynamo.define` for single-table\ndesign: many entity types in one physical table, with key templates, an\n`entityType` discriminator, and splitters for heterogeneous result sets. It\ncomposes the normal Model API (it doesn't replace it).\n\n```ts\nimport dynamo, { z, defineSingleTable } from '@asabytes/dynamodb';\n\nconst app = defineSingleTable({\n  tableName: 'AppTable',\n  // defaults: hashKey 'PK', rangeKey 'SK', entityTypeAttr 'entityType'\n  indexes: [{ hashKey: 'GSI1PK', rangeKey: 'GSI1SK', name: 'GSI1', type: 'global' }],\n});\n\nconst User = app.entity('User', {\n  // entityType defaults to the name uppercased ('User' -> 'USER'); override if needed\n  schema: { userId: z.string(), email: z.string(), name: z.string().optional() },\n  keys: {                                   // key templates, run on create/update\n    PK: (u) => `USER#${u.userId}`,\n    SK: () => 'PROFILE',\n    GSI1PK: (u) => `EMAIL#${u.email}`,\n    GSI1SK: (u) => `USER#${u.userId}`,\n  },\n});\n\nconst Order = app.entity('Order', {\n  schema: { userId: z.string(), orderId: z.string(), total: z.number() },\n  keys: {\n    PK: (o) => `USER#${o.userId}`,\n    SK: (o) => `ORDER#${o.orderId}`,\n  },\n});\n\n// Create the shared table once (not via dynamo.createTables()).\nawait app.createTable();\n\n// Writes auto-compute PK/SK/GSI keys + entityType — you pass only domain attrs:\nawait User.create({ userId: '1', email: 'a@b.com', name: 'Foo' });\nawait Order.create({ userId: '1', orderId: '9', total: 42 });\n\n// Read by key components (builds the key for you):\nconst user = await User.lookup({ userId: '1' });\nawait Order.remove({ userId: '1', orderId: '9' });\n\n// Item-collection query, then split the heterogeneous result by entity:\nconst res = await app.query('USER#1').where('SK').beginsWith('ORDER#').exec();\nconst orders = res.Items.filter(Order.is); // typed Item<Order attrs>[]\nconst grouped = app.group(res.Items);      // { USER: [...], ORDER: [...] }\n```\n\nNotes:\n- Each entity is a normal model; `User.create/update/get/...` all work, and\n  `User.before('create', …)` runs after the key templates.\n- `app.query(pk)` / `app.scan()` go through a shared loose-schema base model, so\n  results are untyped (`Item<Record<string, any>>`) — narrow with `Entity.is`.\n- Create the table with `app.createTable()`. Don't use the global\n  `dynamo.createTables()` here — every entity model points at the same physical\n  table, so it would issue redundant create/update calls.\n- For updates, pass the attributes the key templates need (e.g. `orderId`) so the\n  key can be recomputed.\n\n## Hooks\n\n`before` hooks receive the data and return the (optionally transformed) data;\n`after` hooks receive the resulting item.\n\n```ts\nAccount.before('create', async (data) => ({ ...data, name: data.name?.trim() }));\nAccount.after('create', (item) => console.log('created', item?.get('email')));\n```\n\n## Logging\n\n```ts\ndynamo.log.level('info');     // global\nAccount.log.level('warn');    // per-model\n```\n\n## Migration notes (vs. baseprime/dynamodb)\n\n| Original | This port |\n| --- | --- |\n| Joi schemas (`Joi.string()`) | Zod schemas (`z.string()`), via `import { z }` |\n| `dynamo.types.*` (Joi-backed) | `dynamo.types.*` (Zod-backed) — same names |\n| `aws-sdk` v2 + DocumentClient | `@aws-sdk/client-dynamodb` + `@aws-sdk/lib-dynamodb` |\n| `dynamo.AWS.config.update(...)` | `dynamo.dynamoDriver(new DynamoDBClient({...}))` |\n| callbacks **or** promises | promises / async-await only |\n| Node `Readable` streams | async iterators (`for await...of`, `.pages()`, `.items()`) |\n| `dynamo.Set(values, 'S')` | `dynamo.Set(values)` — a native `Set` (SDK v3 marshals it) |\n| validation errors = Joi error | validation errors = `ZodError` |\n\nThe model-definition, serialization, expression-building, indexing, batch, and\nparallel-scan logic is otherwise a 1:1 port.\n\n## Scripts\n\n```bash\nnpm run build       # bundle to dist/ (ESM + CJS + .d.ts) with tsup\nnpm run typecheck   # tsc --noEmit\nnpm test            # run the Jest suite\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}