{"id":15296027,"url":"https://github.com/davidje13/collection-storage","last_synced_at":"2026-01-05T22:35:47.194Z","repository":{"id":57203175,"uuid":"185077697","full_name":"davidje13/collection-storage","owner":"davidje13","description":"abstraction layer around communication with a collection-based database","archived":false,"fork":false,"pushed_at":"2020-12-27T20:19:26.000Z","size":864,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-03T18:49:21.585Z","etag":null,"topics":["collection","dynamodb","in-memory","mongo","mongodb","nodejs","nosql","persistence","postgresql","redis"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/davidje13.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-05T20:13:54.000Z","updated_at":"2020-12-27T21:06:20.000Z","dependencies_parsed_at":"2022-09-17T18:12:39.770Z","dependency_job_id":null,"html_url":"https://github.com/davidje13/collection-storage","commit_stats":null,"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fcollection-storage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fcollection-storage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fcollection-storage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fcollection-storage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidje13","download_url":"https://codeload.github.com/davidje13/collection-storage/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245407755,"owners_count":20610232,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collection","dynamodb","in-memory","mongo","mongodb","nodejs","nosql","persistence","postgresql","redis"],"created_at":"2024-09-30T18:09:00.808Z","updated_at":"2026-01-05T22:35:47.150Z","avatar_url":"https://github.com/davidje13.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Collection Storage\n\nProvides an abstraction layer around communication with a\ncollection-based database. This makes switching database choices easier\nduring deployments and testing.\n\nCurrently supports MongoDB, DynamoDB, Redis (experimental), PostgreSQL, and\nin-memory storage.\n\n## Install dependency\n\n```bash\nnpm install --save collection-storage\n```\n\nIf you want to connect to a Mongo database, you will also need to add a\ndependency on `mongodb`:\n\n```bash\nnpm install --save mongodb\n```\n\nIf you want to connect to a Redis database, you will also need to add a\ndependency on `ioredis`:\n\n```bash\nnpm install --save ioredis\n```\n\n**warning**: Redis support is experimental and the database format is likely\nto change in later versions.\n\nIf you want to connect to a PostgreSQL database, you will also need to add a\ndependency on `pg`:\n\n```bash\nnpm install --save pg\n```\n\n**note**: Though PostgreSQL is supported, it is not optimised for this type of\ndata storage. If possible, use one of the NoSQL options instead.\n\nYou do not need any additional dependencies to connect to an in-memory or\nDynamoDB database.\n\n## Usage\n\n```javascript\nimport CollectionStorage from 'collection-storage';\n\nconst dbUrl = 'memory://something';\n\nasync function example() {\n  const db = await CollectionStorage.connect(dbUrl);\n\n  const simpleCol = db.getCollection('simple');\n  await simpleCol.add({ id: 10, message: 'Hello' });\n  const value = await simpleCol.get('id', 10);\n  // value is { id: 10, message: 'Hello' }\n\n  const indexedCol = db.getCollection('complex', {\n    foo: {},\n    bar: { unique: true },\n    baz: {},\n  });\n  await indexedCol.add({ id: 2, foo: 'abc', bar: 'def', baz: 'ghi' });\n  await indexedCol.add({ id: 3, foo: 'ABC', bar: 'DEF', baz: 'ghi' });\n  const found = await indexedCol.getAll('baz', 'ghi');\n  // found is [{ id: 2, ... }, { id: 3, ... }]\n\n  // Next line throws an exception due to the duplicate key in 'bar'\n  await indexedCol.add({ id: 4, foo: 'woo', bar: 'def', baz: 'xyz' });\n\n  // Binary data\n  const binaryCol = db.getCollection('my-binary-collection');\n  await binaryCol.add({ id: 10, someData: Buffer.from('abc', 'utf8') });\n  const data = await binaryCol.get('id', 10);\n  // data.someData is a Buffer\n}\n```\n\nThe unindexed properties of your items do not need to be consistent.\nIn particular, this means that later versions of your application are\nfree to change the unindexed attributes, and both versions can\nco-exist (see [migrate](#migrated) below for details on enabling\nautomatic migrations on a per-record basis).\n\nThe MongoDB and PostgreSQL databases support changing indices in any\nway at a later point. In a later deploy, you can simply create your\ncollection with different indices, and the necessary changes will\nhappen automatically. DynamoDB indices will also be updated\nautomatically but note that this may take some time and will use up\ncapacity on the indices. Note that Redis does not currently support\nchanging or removing existing indices, and will not index existing\ndata if a new index is added.\n\n## Connection Strings\n\n### In-memory\n\n```\nmemory://\u003cidentifier\u003e[?options]\n```\n\nThe in-memory database stores data in `Map`s and `Set`s. This data is\nnot stored to disk, so when the application closes it is gone. If you\nspecify an identifier, subsequent calls using the same identifier\nwithin the same process will access the same database. If you specify\nno identifier, the database will always be created fresh.\n\n#### Options\n\n* `simulatedLatency=\u003cmilliseconds\u003e`: enforces a delay of the given\n  duration whenever data is read or written. This can be used to\n  simulate communication with a remote database to ensure that tests do\n  not contain race conditions.\n\n### MongoDB\n\n```\nmongodb://[username:password@]host1[:port1][,...hostN[:portN]]][/[database][?options]]\n```\n\nSee the [mongo documentation](https://docs.mongodb.com/manual/reference/connection-string/)\nfor full details.\n\n### DynamoDB\n\n```\ndynamodb://[key:secret@]dynamodb.region.amazonaws.com[:port]/[table-prefix-][?options]\n```\n\nSee the [AWS documentation](https://docs.aws.amazon.com/general/latest/gr/rande.html)\nfor a list of region names. Requests will use `https` by default. Specify\n`tls=false` in the options to switch to `http` (e.g. when using DynamoDB\nLocal for testing.)\n\nBy default, eventually-consistent reads are used. To use strongly-consistent\nreads, specify `consistentRead=true` (note that this will use twice as much\nread capacity for the same operations).\n\nTo configure read/write capacity for tables, see the section below (but\nnote that it is recommended to keep the default pay-per-request and\nconfigure provisioned throughput externally once the usage is known).\n\n### Redis\n\n```\nredis://[username:password@]host[:port][/[database-index][?options]]\nrediss://[username:password@]host[:port][/[database-index][?options]]\n```\n\nSee the [ioredis documentation](https://github.com/luin/ioredis#readme)\nfor more details.\n\n### PostgreSQL\n\n```\npostgresql://[username:password@]host[:port]/database[?options]\n```\n\nOptions can include `ssl=true`, `sslcert=\u003ccert-file-path\u003e`,\n`sslkey=\u003ckey-file-path\u003e`, `sslrootcert=\u003croot-file-path\u003e`. For other options,\nsee the config keys in the\n[pg Client constructor documentation](https://node-postgres.com/api/client/#constructor).\n\n## Encryption\n\nYou can enable client-side encryption by wrapping the collections:\n\nThe encryption used is aes-256-cbc.\n\nAny provided keys (`encryptByKey`) are not stored externally and never leave\nthe server. These keys must remain constant through restarts and redeploys,\nand must be the same on all load-balanced instances. Generated keys\n(`encryptByRecord`) are stored in a provided collection (which does not have\nto be in the same database, or even in the same database type), and can be\nencrypted using a provided key which is not stored.\n\n```javascript\nimport CollectionStorage, {\n  encryptByKey,\n  encryptByRecord,\n  encryptByRecordWithMasterKey,\n} from 'collection-storage';\n\nconst dbUrl = 'memory://something';\n\nasync function example() {\n  const db = await CollectionStorage.connect(dbUrl);\n\n  // input keys must be 32 bytes, e.g.:\n  const rootKey = crypto.randomBytes(32);\n\n  // Option 1: single key for all values\n  const enc1 = encryptByKey(rootKey);\n  const simpleCol1 = enc1(['foo'], db.getCollection('simple1'));\n\n  // Option 2: unique key per value, non-encrypted key\n  const keyCol2 = db.getCollection('keys2');\n  const enc2 = encryptByRecord(keyCol2, { keyCache: { capacity: 50 } });\n  const simpleCol2 = enc2(['foo'], db.getCollection('simple2'));\n\n  // Option 3 (recommended): unique key per value, encrypted using global key\n  const keyCol3 = db.getCollection('keys3');\n  const enc3 = encryptByRecordWithMasterKey(rootKey, keyCol3, { keyCache: { capacity: 50 } });\n  const simpleCol3 = enc3(['foo'], db.getCollection('simple3'));\n\n  // option 3 is equivalent to:\n  const keyCol4 = encryptByKey(rootKey)(['key'], db.getCollection('keys4'));\n  const enc4 = encryptByRecord(keyCol4, { keyCache: { capacity: 50 } });\n  const simpleCol4 = enc4(['foo'], db.getCollection('simple4'));\n\n  // For all options, the encryption is transparent:\n  await simpleCol1.add({ id: 10, foo: 'This is encrypted' });\n  const value1 = await simpleCol1.get('id', 10);\n  // value1 is { id: 10, foo: 'This is encrypted' }\n}\n```\n\nNotes:\n\n* You cannot query using encrypted columns\n* By default, encryption and decryption is done *synchronously* via the\n  built-in `crypto` APIs.\n\nTo use another library for cryptography (e.g. to enable asynchronous\noperations), you can provide a final parameter to the `encryptBy*` function:\n\n```javascript\nconst myEncryption = {\n  encrypt: async (key, input) =\u003e {\n    // input (Buffer) =\u003e encrypted (Buffer)\n  },\n\n  decrypt: async (key, encrypted) =\u003e {\n    // encrypted (Buffer) =\u003e value (Buffer)\n  },\n\n  generateKey: () =\u003e {\n    // return a random key\n    // this will be passed to the encrypt/decrypt functions as `key`\n  },\n\n  serialiseKey: (key) =\u003e {\n    // return a string representation of key\n  },\n\n  deserialiseKey: (data) =\u003e {\n    // reverse of serialiseKey\n  },\n};\n\nconst enc = encryptByKey(rootKey, { encryption: myEncryption });\n```\n\n## Compression\n\nSee the documentation for [compress](#compressed) below for details on\nenabling automatic compression of values.\n\n## Per-record Migration\n\nSee the documentation for [migrate](#migrated) below for details on\nenabling automatic migrations on a per-record basis.\n\n## Caching\n\nSee the documentation for [cache](#cached) below for details on\nenabling automatic caching of items.\n\n## API\n\n### CollectionStorage\n\n#### `connect`\n\n```javascript\nconst db = await CollectionStorage.connect(url);\n```\n\nConnects to the given database and returns a database wrapper.\n\n### Database\n\n#### `getCollection`\n\n```javascript\nconst collection = db.getCollection(name, [keys]);\n```\n\nInitialises the requested collection in the database and returns a\ncollection wrapper.\n\n`keys` is an optional object defining the searchable keys for the\ncollection. For example:\n\n```javascript\nconst collection = await db.getCollection(name, {\n  someSimpleKey: {},\n  someUniqueKey: { unique: true },\n  anotherSimpleKey: {},\n});\n```\n\nThe `id` attribute is always indexed and should not be specified\nexplicitly.\n\n#### `close`\n\n```javascript\nawait db.close();\n```\n\nDisconnects from the database. Any in-progress operations will\ncomplete, but any new operations will fail with an exception.\n\nThe database object cannot be reused after calling `close`.\n\nThe returned promise will resolve once all in-progress operations\nhave completed and all connections have fully closed.\n\n### Collection\n\n#### `add`\n\n```javascript\nawait collection.add(value);\n```\n\nAdds the given value to the collection. `value` should be an object\nwith an `id` and any other fields you wish to save.\n\n#### `update`\n\n```javascript\nawait collection.update(searchAttr, searchValue, update, [options]);\n```\n\nUpdates all entries which match `searchAttr = searchValue`. Any\nattributes not specified in `update` will remain unchanged.\n\nThe `searchAttr` can be any indexed attribute (including `id`).\n\nWhen using a non-unique index, only non-unique values can be\nspecified, even if the data contains only one matching entry.\n\nIf `options` is `{ upsert: true }` and no values match the search, a\nnew entry will be added. If using `upsert` mode, the `searchAttr`\nmust be `id`.\n\n#### `get`\n\n```javascript\nconst value = await collection.get(searchAttr, searchValue, [attrs]);\n```\n\nReturns one entry which matches `searchAttr = searchValue`. If `attrs`\nis specified, only the attributes listed will be returned (by default,\nall attributes are returned).\n\nThe `searchAttr` can be any indexed attribute (including `id`).\n\n`attrs` is an optional list of strings denoting the attributes to\nreturn.\n\nIf no values match, returns `null`.\n\n#### `getAll`\n\n```javascript\nconst values = await collection.getAll(searchAttr, searchValue, [attrs]);\n```\n\nLike `get`, but returns a list of all matching values. If no values\nmatch, returns an empty list.\n\n#### `remove`\n\n```javascript\nconst count = await collection.remove(searchAttr, searchValue);\n```\n\nRemoves all entries matching `searchAttr = searchValue`.\n\nThe `searchAttr` can be any indexed attribute (including `id`).\n\nReturns the number of records removed (0 if no records matched).\n\n### Encrypted\n\n#### `encryptByKey`\n\n```javascript\nconst enc = encryptByKey(key, [options]);\nconst collection = enc(['encryptedField', 'another'], baseCollection);\n```\n\nReturns a function which can wrap collections with encryption.\n\nBy default the provided `key` should be a 32-byte buffer.\nIf custom encryption is used, the key should conform to its expectations.\n\nSee example notes above for an example on using `options.encryption`.\n\nIf `options.allowRaw` is `true`, unencrypted values will be passed through.\nThis can be useful when migrating old columns to use encryption. Note that\nbuffer (binary) data will _always_ be decrypted; never passed through.\n\n#### `encryptByRecord`\n\n```javascript\nconst enc = encryptByRecord(keyCollection, [options]);\nconst collection = enc(['myEncryptedField', 'another'], baseCollection);\n```\n\nReturns a function which can wrap collections with encryption.\n\nStores one key per ID in `keyCollection` (unencrypted).\nIf `options.keyCache` is provided, uses a least-recently-used cache for keys\nto reduce database access. `keyCache` should be set to an object which\ncontains the settings described for [cache](#cached).\n\nUpdating a record re-encrypts using the same key. Removing records also\nremoves the corresponding keys.\n\nSee example notes above for an example on using `options.encryption`.\n\nIf `options.allowRaw` is `true`, unencrypted values will be passed through.\nThis can be useful when migrating old columns to use encryption. Note that\nbuffer (binary) data will _always_ be decrypted; never passed through.\n\n#### `encryptByRecordWithMasterKey`\n\n```javascript\nconst enc = encryptByRecordWithMasterKey(masterKey, keyCollection, [options]);\nconst collection = enc(['myEncryptedField', 'another'], baseCollection);\n```\n\nReturns a function which can wrap collections with encryption.\n\nStores one key per ID in `keyCollection` (encrypted using `masterKey`).\nIf `options.keyCache` is provided, uses a least-recently-used cache for keys\nto reduce database access. `keyCache` should be set to an object which\ncontains the settings described for [cache](#cached).\n\nThis is equivalent to:\n\n```javascript\nconst keys = encryptByKey(masterKey, [options])(keyCollection, ['key']);\nconst enc = encryptByRecord(keys, [options]);\nconst collection = enc(['myEncryptedField', 'another'], baseCollection);\n```\n\nSee example notes above for an example on using `options.encryption`.\n\n### Compressed\n\n#### `compress`\n\n```javascript\nconst collection = compress(['compressedField', 'another'], baseCollection);\n```\n\nWraps a collection with compression. Uses gzip compression and ensures that\nshort uncompressable messages will not grow significantly (2 bytes maximum).\n\nIf you apply compression to an existing column, old (uncompressed) values\nwill be passed through automatically (except binary data). To disable this\nfunctionality, pass `allowRaw: false`:\n\n```javascript\nconst collection = compress(['value'], baseCollection, { allowRaw: false });\n```\n\nIf you are migrating a column which contains binary data, you should\nprobably migrate the data to add compression (or at least prefix all values\nwith a 0x00 byte to mark them uncompressed). If this is not possible, you\ncan pass `allowRawBuffer: true` to `compress` but **note**: any data which\nbegins with `0x00` will have that byte stripped. Additionally, any data which\nhappens to start with `0x1f 0x8b` (the gzip \"magic number\") will be passed\nthrough `zlib.gunzip`. Enabling `allowRawBuffer` is provided as an escape\nhatch, but is _not recommended_.\n\nDo not apply compression to short values, or values with no compressible\nstructure (e.g. pre-compressed images, random data); it will increase the\nsize rather than reduce it. By default, compression is not attempted for\nvalues which are less than 200 bytes. You can change this with\n`options.compressionThresholdBytes`; smaller values may result in minor byte\nsavings, but will require more CPU (note that there is no point setting the\nthreshold less than 12 as gzip always adds 11 bytes of overhead).\n\n##### `compress` \u0026 `encrypt`\n\nIf you want to use compression in combination with encryption, note that you\nshould compress *then* encrypt. Once data has been encrypted, compression will\nhave little effect. Also beware: if your application allows writing part of a\ncompressed field, and the database is exposed, it will be possible for an\nattacker to use compression, along with observation of the resulting record\nsize, to guess secrets from the same value which may otherwise be hidden to\nthem. Data in separate fields which an attacker cannot control will remain\nsafe, even if compressed. This is a rare situation but should be considered\nwhen encrypting any compressed data.\n\n```javascript\nconst fields = ['field', 'another'];\nconst enc = encryptByKey(key);\n// be sure to apply compression and encryption in the correct order!\nconst collection = compress(fields, enc(fields, baseCollection));\n```\n\n### Cached\n\n#### `cache`\n\n```javascript\nconst collection = cache(baseCollection, [options]);\n```\n\nWraps a collection with read caching. Writes will still be recorded immediately\nand will be reflected in the cached data, but changes made by other clients\nwill not be returned until the cache is deemed stale.\n\nThis adds a small overhead to the backing collection as it will fetch the ID\nattribute for most operations even if not requested, but the ability to return\ncached data should outweigh this cost in almost all cases.\n\nBy default, items in the cache never expire (unless found to be invalid when\nperforming other operations, such as successfully reusing a unique index value)\nand the cache has an unlimited size. In real applications, this is unlikely to\nbe desirable. You can configure the cache with the `options` object:\n\n```javascript\nconst collection = cache(baseCollection, {\n  capacity: 128, // number of records to store (oldest items are removed)\n  maxAge: 1000, // max age in milliseconds\n});\n```\n\n`capacity` and `maxAge` default to infinity. Note that items which expire\ndue to `maxAge` will _not_ be removed from the cache automatically. You\nshould specify a `capacity` to keep the cache from growing infinitely even\nwhen using a `maxAge`.\n\nIf you want to test situations where the cache has expired, you can also\nspecify `time`. This should be a function compatible with the `Date.now`\nsignature (`Date.now` is the default).\n\n### Migrated\n\n#### `migrate`\n\n```javascript\nconst collection = migrate({\n  migratedField: (stored) =\u003e newValue,\n  another: (stored) =\u003e newValue\n}, baseCollection);\n```\n\n```javascript\nconst collection = migrate(['versionColumn'], {\n  migratedField: (stored, { versionColumn }) =\u003e newValue,\n  another: (stored, { versionColumn }) =\u003e newValue\n}, baseCollection);\n```\n\nWraps a collection with an automatic on-fetch migration. The migrations will\nbe applied whenever records are read, but will not be saved back into the\ndatabase. The migration functions are per-field, taking in the old field\nvalue and returning an updated field value. Each function will only be\ninvoked if the user requested that particular field.\n\nIf version information is required to decide whether to migrate or not,\nadditional fields to fetch can be specified and these will be made available\nto all migration functions in the second function parameter. It is up to you\nto write the appropriate version to this field when adding or updating\nvalues. You can specify as many extra fields as you need (e.g. to allow one\nversion field for each field, or to include other fields which are used to\nderive new values).\n\n## Specifying provisioned capacity for DynamoDB\n\nWhen using DynamoDB, it is possible to specify explicit read/write capacity\nfor each table. By default, all tables are configured as pay-per-request.\nNote that this will only affect the initial table creation; no automatic\nmigration of provisioned capacity is currently applied.\n\nTypically it is recommended to start with pay-per-request (the default) and\nconfigure provisioned capacity once you know what the usage of your tables\nwill be in production. This can be done outside the application, either\nusing the AWS console manually, or the CLI for automation. But if you know\nthe usage in advance and want to specify it on table creation, this library\nallows you to do so.\n\nTo specify explicit provisioned capacities, either:\n\n- Specify capacities in the connection string:\n\n  ```\n  - Only do this if you know what you are doing!\n  - If used incorrectly, this can make DynamoDB cost more.\n  dynamodb://dynamodb.eu-west-1.amazonaws.com/\n    ?provision_my-hot-table=10.2\n    \u0026provision_my-hot-table_index_my-special-index=2.1\n    \u0026provision_my-hot-table_index=4.2\n    \u0026provision=-\n  ```\n\n  (newlines added for clarity, but must not be present in the actual\n  connection string)\n\n  The formats recognised are:\n\n  ```\n  fallback for all tables and indices:\n  provision=\u003cread\u003e.\u003cwrite\u003e\n\n  explicit config for \u003ctable-name\u003e:\n  provision_\u003ctable-name\u003e=\u003cread\u003e.\u003cwrite\u003e\n\n  fallback for all indices of \u003ctable-name\u003e:\n  provision_\u003ctable-name\u003e_index=\u003cread\u003e.\u003cwrite\u003e\n\n  explicit config for \u003cindex-name\u003e of \u003ctable-name\u003e:\n  provision_\u003ctable-name\u003e_index_\u003cindex-name\u003e=\u003cread\u003e.\u003cwrite\u003e\n  ```\n\n  Setting any property to a dash (`-`) will use pay-per-request billing.\n\n- Or, if calling `DynamoDb.connect` directly, you can specify a function\n  as the second parameter to allow programmatic control:\n\n  ```javascript\n  function myThroughput(tableName, indexName) {\n    // Only do this if you know what you are doing!\n    // If used incorrectly, this can make DynamoDB cost more.\n    switch (tableName) {\n      case 'my-hot-table':\n        switch (indexName) {\n          case null:\n            // applies to the table my-hot-table\n            return { read: 10, write: 2 };\n          case 'my-special-index':\n            // applies to my-special-index for my-hot-table\n            return { read: 2, write: 1 };\n          default:\n            // applies to all other indices for my-hot-table\n            return { read: 4, write: 2 };\n        }\n      default:\n        // applies to all other tables and indices\n        return null; // use pay-per-request\n    }\n  }\n\n  const db = DynamoDb.connect('dynamodb://etc', myThroughput);\n  ```\n\n  The function is called once with a `null` index name for the base table\n  properties, and once per index for the index properties.\n\n  Returning `null` or `undefined` will cause that table to use\n  pay-per-request billing.\n\nNotes for both methods:\n\n- Table names and index names will be the raw names before any common\n  prefix is added.\n\n- Unique indices are all bundled into a single table, so the provisioned\n  values for these are summed together for that table.\n\n- The provisioned units should always be integers, but are automatically\n  rounded (using `ceil`) and clamped to a minimum of 1.\n\n- DynamoDB does not allow using a mix of provisioned and pay-per-request\n  billing for a table and its indices. Set each table and its indices\n  either all pay-per-request or all provisioned.\n\n## Development\n\nTo run the test suite, you will need to have a local installation of MongoDB,\nRedis, PostgreSQL and DynamoDB Local. By default, the tests will connect to\n`mongodb://localhost:27017/collection-storage-tests`,\n`redis://localhost:6379/15`,\n`postgresql://localhost:5432/collection-storage-tests`, and\n`dynamodb://key:secret@localhost:8000/collection-storage-tests-?tls=false`.\nYou can change this if required by setting the `MONGO_URL`, `REDIS_URL`,\n`PSQL_URL`, and `DDB_URL` environment variables.\n\n**warning**: By default, this will flush any Redis database at index 15. If\nyou have used database 15 for your own data, you should set `REDIS_URL` to\nuse a different database index.\n\n**note**: The PostgreSQL tests will connect to the given server's `postgres`\ndatabase to drop (if necessary) and re-create the specified test database.\nYou do not need to create the test database yourself.\n\nThe target databases can be started using Docker if not installed locally:\n\n```bash\ndocker run -d -p 27017:27017 mongo:4\ndocker run -d -p 6379:6379 redis:5-alpine\ndocker run -d -p 5432:5432 postgres:11-alpine\ndocker run -d -p 8000:8000 amazon/dynamodb-local:latest\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidje13%2Fcollection-storage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidje13%2Fcollection-storage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidje13%2Fcollection-storage/lists"}