@keyv/dynamo 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,88 @@ e.g:
35
35
  const keyv = new KeyvDynamo({ tableName: 'cacheTable' });
36
36
  ```
37
37
 
38
+ ### Accessing the DynamoDB Client
39
+
40
+ The DynamoDB client is exposed as a property for advanced use cases:
41
+
42
+ ```js
43
+ const store = new KeyvDynamo();
44
+ const dynamoClient = store.client; // DynamoDBDocument instance
45
+
46
+ // You can use the client directly for custom operations
47
+ await dynamoClient.get({ TableName: 'keyv', Key: { id: 'myKey' } });
48
+ ```
49
+
50
+ ## Usage with NestJS
51
+
52
+ Since DynamoDB has a 400KB limit per item, compressing data can help in some cases.
53
+
54
+ ### With a payload less than or equal to 400KB
55
+
56
+ ```js
57
+ import { Keyv } from 'keyv'
58
+ import { KeyvDynamo } from '@keyv/dynamo'
59
+ import { DynamoModule } from '@lyfe/dynamo-module'
60
+ import { CacheModule } from '@nestjs/cache-manager'
61
+ import { Module } from '@nestjs/common'
62
+
63
+ @Module({
64
+ imports: [
65
+ CacheModule.registerAsync({
66
+ isGlobal: true,
67
+ useFactory: async () => {
68
+ return {
69
+ stores: [
70
+ new Keyv({
71
+ store: new KeyvDynamo({
72
+ tableName: 'TableName',
73
+ }),
74
+ }),
75
+ ],
76
+ }
77
+ },
78
+ }),
79
+ ],
80
+ exports: [DynamoModule],
81
+ })
82
+ export class InfrastructureModule {}
83
+
84
+ ```
85
+
86
+ ### With a payload greater than 400KB
87
+
88
+ ```js
89
+ import { Keyv } from 'keyv'
90
+ import KeyvBrotli from '@keyv/compress-brotli'
91
+ import { KeyvDynamo } from '@keyv/dynamo'
92
+ import { DynamoModule } from '@lyfe/dynamo-module'
93
+ import { CacheModule } from '@nestjs/cache-manager'
94
+ import { Module } from '@nestjs/common'
95
+
96
+ @Module({
97
+ imports: [
98
+ CacheModule.registerAsync({
99
+ isGlobal: true,
100
+ useFactory: async () => {
101
+ return {
102
+ stores: [
103
+ new Keyv({
104
+ store: new KeyvDynamo({
105
+ tableName: 'TableName',
106
+ }),
107
+ compression: new KeyvBrotli(),
108
+ }),
109
+ ],
110
+ }
111
+ },
112
+ }),
113
+ ],
114
+ exports: [DynamoModule],
115
+ })
116
+ export class InfrastructureModule {}
117
+
118
+ ```
119
+
38
120
  ## License
39
121
 
40
122
  [MIT © Jared Wray](LISCENCE)
package/dist/index.cjs CHANGED
@@ -34,15 +34,16 @@ __export(index_exports, {
34
34
  default: () => index_default
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_events = __toESM(require("events"), 1);
37
+ var import_node_events = __toESM(require("events"), 1);
38
38
  var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
39
39
  var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
40
- var KeyvDynamo = class extends import_events.default {
40
+ var KeyvDynamo = class extends import_node_events.default {
41
41
  ttlSupport = true;
42
42
  sixHoursInMilliseconds = 6 * 60 * 60 * 1e3;
43
43
  namespace;
44
44
  opts;
45
45
  client;
46
+ tableReady;
46
47
  constructor(options) {
47
48
  super();
48
49
  options ??= {};
@@ -55,15 +56,17 @@ var KeyvDynamo = class extends import_events.default {
55
56
  ...options
56
57
  };
57
58
  this.client = import_lib_dynamodb.DynamoDBDocument.from(new import_client_dynamodb.DynamoDB(this.opts));
58
- this.checkTableExists(this.opts.tableName).catch((error) => {
59
- this.emit("error", error);
60
- });
61
- }
62
- async checkTableExists(tableName) {
63
- await this.client.send(new import_client_dynamodb.DescribeTableCommand({ TableName: tableName }));
59
+ this.tableReady = this.ensureTable(this.opts.tableName).catch(
60
+ (error) => {
61
+ this.emit("error", error);
62
+ }
63
+ );
64
64
  }
65
65
  async set(key, value, ttl) {
66
- const sixHoursFromNowEpoch = Math.floor((Date.now() + this.sixHoursInMilliseconds) / 1e3);
66
+ await this.tableReady;
67
+ const sixHoursFromNowEpoch = Math.floor(
68
+ (Date.now() + this.sixHoursInMilliseconds) / 1e3
69
+ );
67
70
  const expiresAt = typeof ttl === "number" ? Math.floor((Date.now() + (ttl + 1e3)) / 1e3) : sixHoursFromNowEpoch;
68
71
  const putInput = {
69
72
  TableName: this.opts.tableName,
@@ -76,6 +79,7 @@ var KeyvDynamo = class extends import_events.default {
76
79
  await this.client.put(putInput);
77
80
  }
78
81
  async get(key) {
82
+ await this.tableReady;
79
83
  const getInput = {
80
84
  TableName: this.opts.tableName,
81
85
  Key: {
@@ -86,6 +90,7 @@ var KeyvDynamo = class extends import_events.default {
86
90
  return Item?.value;
87
91
  }
88
92
  async delete(key) {
93
+ await this.tableReady;
89
94
  const deleteInput = {
90
95
  TableName: this.opts.tableName,
91
96
  Key: {
@@ -97,6 +102,7 @@ var KeyvDynamo = class extends import_events.default {
97
102
  return Boolean(Attributes);
98
103
  }
99
104
  async getMany(keys) {
105
+ await this.tableReady;
100
106
  const batchGetInput = {
101
107
  RequestItems: {
102
108
  [this.opts.tableName]: {
@@ -107,9 +113,12 @@ var KeyvDynamo = class extends import_events.default {
107
113
  }
108
114
  };
109
115
  const { Responses: { [this.opts.tableName]: items = [] } = {} } = await this.client.batchGet(batchGetInput);
110
- return keys.map((key) => items.find((item) => item?.id === key)?.value);
116
+ return keys.map(
117
+ (key) => items.find((item) => item?.id === key)?.value
118
+ );
111
119
  }
112
120
  async deleteMany(keys) {
121
+ await this.tableReady;
113
122
  if (keys.length === 0) {
114
123
  return false;
115
124
  }
@@ -133,6 +142,7 @@ var KeyvDynamo = class extends import_events.default {
133
142
  return Boolean(response);
134
143
  }
135
144
  async clear() {
145
+ await this.tableReady;
136
146
  const scanResult = await this.client.scan({
137
147
  TableName: this.opts.tableName
138
148
  });
@@ -142,6 +152,53 @@ var KeyvDynamo = class extends import_events.default {
142
152
  extractKey(output, keyProperty = "id") {
143
153
  return (output.Items ?? []).map((item) => item[keyProperty]).filter((key) => key.startsWith(this.namespace ?? ""));
144
154
  }
155
+ async ensureTable(tableName) {
156
+ try {
157
+ await this.client.send(
158
+ new import_client_dynamodb.DescribeTableCommand({ TableName: tableName })
159
+ );
160
+ } catch (error) {
161
+ if (error instanceof import_client_dynamodb.ResourceNotFoundException) {
162
+ await this.createTable(tableName);
163
+ } else {
164
+ throw error;
165
+ }
166
+ }
167
+ }
168
+ async createTable(tableName) {
169
+ try {
170
+ await this.client.send(
171
+ new import_client_dynamodb.CreateTableCommand({
172
+ TableName: tableName,
173
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
174
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
175
+ BillingMode: "PAY_PER_REQUEST"
176
+ })
177
+ );
178
+ await (0, import_client_dynamodb.waitUntilTableExists)(
179
+ { client: this.client, maxWaitTime: 60 },
180
+ { TableName: tableName }
181
+ );
182
+ await this.client.send(
183
+ new import_client_dynamodb.UpdateTimeToLiveCommand({
184
+ TableName: tableName,
185
+ TimeToLiveSpecification: {
186
+ AttributeName: "expiresAt",
187
+ Enabled: true
188
+ }
189
+ })
190
+ );
191
+ } catch (error) {
192
+ if (error instanceof import_client_dynamodb.ResourceInUseException) {
193
+ await (0, import_client_dynamodb.waitUntilTableExists)(
194
+ { client: this.client, maxWaitTime: 60 },
195
+ { TableName: tableName }
196
+ );
197
+ } else {
198
+ throw error;
199
+ }
200
+ }
201
+ }
145
202
  };
146
203
  var index_default = KeyvDynamo;
147
204
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -1,17 +1,18 @@
1
- import EventEmitter from 'events';
1
+ import EventEmitter from 'node:events';
2
2
  import { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
3
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
3
4
  import { KeyvStoreAdapter, StoredData } from 'keyv';
4
5
 
5
6
  declare class KeyvDynamo extends EventEmitter implements KeyvStoreAdapter {
6
7
  ttlSupport: boolean;
7
8
  sixHoursInMilliseconds: number;
8
9
  namespace?: string;
9
- opts: Omit<KeyvDynamoOptions, 'tableName'> & {
10
+ opts: Omit<KeyvDynamoOptions, "tableName"> & {
10
11
  tableName: string;
11
12
  };
12
- private readonly client;
13
+ readonly client: DynamoDBDocument;
14
+ private readonly tableReady;
13
15
  constructor(options: KeyvDynamoOptions | string);
14
- checkTableExists(tableName: string): Promise<void>;
15
16
  set(key: string, value: unknown, ttl?: number): Promise<void>;
16
17
  get<Value>(key: string): Promise<StoredData<Value>>;
17
18
  delete(key: string): Promise<boolean>;
@@ -19,6 +20,8 @@ declare class KeyvDynamo extends EventEmitter implements KeyvStoreAdapter {
19
20
  deleteMany(keys: string[]): Promise<boolean>;
20
21
  clear(): Promise<void>;
21
22
  private extractKey;
23
+ private ensureTable;
24
+ private createTable;
22
25
  }
23
26
 
24
27
  type KeyvDynamoOptions = {
package/dist/index.d.ts CHANGED
@@ -1,17 +1,18 @@
1
- import EventEmitter from 'events';
1
+ import EventEmitter from 'node:events';
2
2
  import { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
3
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
3
4
  import { KeyvStoreAdapter, StoredData } from 'keyv';
4
5
 
5
6
  declare class KeyvDynamo extends EventEmitter implements KeyvStoreAdapter {
6
7
  ttlSupport: boolean;
7
8
  sixHoursInMilliseconds: number;
8
9
  namespace?: string;
9
- opts: Omit<KeyvDynamoOptions, 'tableName'> & {
10
+ opts: Omit<KeyvDynamoOptions, "tableName"> & {
10
11
  tableName: string;
11
12
  };
12
- private readonly client;
13
+ readonly client: DynamoDBDocument;
14
+ private readonly tableReady;
13
15
  constructor(options: KeyvDynamoOptions | string);
14
- checkTableExists(tableName: string): Promise<void>;
15
16
  set(key: string, value: unknown, ttl?: number): Promise<void>;
16
17
  get<Value>(key: string): Promise<StoredData<Value>>;
17
18
  delete(key: string): Promise<boolean>;
@@ -19,6 +20,8 @@ declare class KeyvDynamo extends EventEmitter implements KeyvStoreAdapter {
19
20
  deleteMany(keys: string[]): Promise<boolean>;
20
21
  clear(): Promise<void>;
21
22
  private extractKey;
23
+ private ensureTable;
24
+ private createTable;
22
25
  }
23
26
 
24
27
  type KeyvDynamoOptions = {
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  // src/index.ts
2
2
  import EventEmitter from "events";
3
3
  import {
4
+ CreateTableCommand,
4
5
  DescribeTableCommand,
5
- DynamoDB
6
+ DynamoDB,
7
+ ResourceInUseException,
8
+ ResourceNotFoundException,
9
+ UpdateTimeToLiveCommand,
10
+ waitUntilTableExists
6
11
  } from "@aws-sdk/client-dynamodb";
7
12
  import {
8
13
  DynamoDBDocument
@@ -13,6 +18,7 @@ var KeyvDynamo = class extends EventEmitter {
13
18
  namespace;
14
19
  opts;
15
20
  client;
21
+ tableReady;
16
22
  constructor(options) {
17
23
  super();
18
24
  options ??= {};
@@ -25,15 +31,17 @@ var KeyvDynamo = class extends EventEmitter {
25
31
  ...options
26
32
  };
27
33
  this.client = DynamoDBDocument.from(new DynamoDB(this.opts));
28
- this.checkTableExists(this.opts.tableName).catch((error) => {
29
- this.emit("error", error);
30
- });
31
- }
32
- async checkTableExists(tableName) {
33
- await this.client.send(new DescribeTableCommand({ TableName: tableName }));
34
+ this.tableReady = this.ensureTable(this.opts.tableName).catch(
35
+ (error) => {
36
+ this.emit("error", error);
37
+ }
38
+ );
34
39
  }
35
40
  async set(key, value, ttl) {
36
- const sixHoursFromNowEpoch = Math.floor((Date.now() + this.sixHoursInMilliseconds) / 1e3);
41
+ await this.tableReady;
42
+ const sixHoursFromNowEpoch = Math.floor(
43
+ (Date.now() + this.sixHoursInMilliseconds) / 1e3
44
+ );
37
45
  const expiresAt = typeof ttl === "number" ? Math.floor((Date.now() + (ttl + 1e3)) / 1e3) : sixHoursFromNowEpoch;
38
46
  const putInput = {
39
47
  TableName: this.opts.tableName,
@@ -46,6 +54,7 @@ var KeyvDynamo = class extends EventEmitter {
46
54
  await this.client.put(putInput);
47
55
  }
48
56
  async get(key) {
57
+ await this.tableReady;
49
58
  const getInput = {
50
59
  TableName: this.opts.tableName,
51
60
  Key: {
@@ -56,6 +65,7 @@ var KeyvDynamo = class extends EventEmitter {
56
65
  return Item?.value;
57
66
  }
58
67
  async delete(key) {
68
+ await this.tableReady;
59
69
  const deleteInput = {
60
70
  TableName: this.opts.tableName,
61
71
  Key: {
@@ -67,6 +77,7 @@ var KeyvDynamo = class extends EventEmitter {
67
77
  return Boolean(Attributes);
68
78
  }
69
79
  async getMany(keys) {
80
+ await this.tableReady;
70
81
  const batchGetInput = {
71
82
  RequestItems: {
72
83
  [this.opts.tableName]: {
@@ -77,9 +88,12 @@ var KeyvDynamo = class extends EventEmitter {
77
88
  }
78
89
  };
79
90
  const { Responses: { [this.opts.tableName]: items = [] } = {} } = await this.client.batchGet(batchGetInput);
80
- return keys.map((key) => items.find((item) => item?.id === key)?.value);
91
+ return keys.map(
92
+ (key) => items.find((item) => item?.id === key)?.value
93
+ );
81
94
  }
82
95
  async deleteMany(keys) {
96
+ await this.tableReady;
83
97
  if (keys.length === 0) {
84
98
  return false;
85
99
  }
@@ -103,6 +117,7 @@ var KeyvDynamo = class extends EventEmitter {
103
117
  return Boolean(response);
104
118
  }
105
119
  async clear() {
120
+ await this.tableReady;
106
121
  const scanResult = await this.client.scan({
107
122
  TableName: this.opts.tableName
108
123
  });
@@ -112,6 +127,53 @@ var KeyvDynamo = class extends EventEmitter {
112
127
  extractKey(output, keyProperty = "id") {
113
128
  return (output.Items ?? []).map((item) => item[keyProperty]).filter((key) => key.startsWith(this.namespace ?? ""));
114
129
  }
130
+ async ensureTable(tableName) {
131
+ try {
132
+ await this.client.send(
133
+ new DescribeTableCommand({ TableName: tableName })
134
+ );
135
+ } catch (error) {
136
+ if (error instanceof ResourceNotFoundException) {
137
+ await this.createTable(tableName);
138
+ } else {
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+ async createTable(tableName) {
144
+ try {
145
+ await this.client.send(
146
+ new CreateTableCommand({
147
+ TableName: tableName,
148
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
149
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
150
+ BillingMode: "PAY_PER_REQUEST"
151
+ })
152
+ );
153
+ await waitUntilTableExists(
154
+ { client: this.client, maxWaitTime: 60 },
155
+ { TableName: tableName }
156
+ );
157
+ await this.client.send(
158
+ new UpdateTimeToLiveCommand({
159
+ TableName: tableName,
160
+ TimeToLiveSpecification: {
161
+ AttributeName: "expiresAt",
162
+ Enabled: true
163
+ }
164
+ })
165
+ );
166
+ } catch (error) {
167
+ if (error instanceof ResourceInUseException) {
168
+ await waitUntilTableExists(
169
+ { client: this.client, maxWaitTime: 60 },
170
+ { TableName: tableName }
171
+ );
172
+ } else {
173
+ throw error;
174
+ }
175
+ }
176
+ }
115
177
  };
116
178
  var index_default = KeyvDynamo;
117
179
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/dynamo",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "DynamoDB storage adapter for Keyv",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -12,29 +12,6 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "xo": {
16
- "space": true,
17
- "rules": {
18
- "import/no-named-as-default": "off",
19
- "unicorn/prefer-module": "off",
20
- "unicorn/prefer-event-target": "off",
21
- "unicorn/no-array-reduce": "off",
22
- "unicorn/prefer-object-from-entries": "off",
23
- "unicorn/prefer-node-protocol": "off",
24
- "@typescript-eslint/no-unsafe-assignment": "off",
25
- "@typescript-eslint/no-unsafe-call": "off",
26
- "@typescript-eslint/no-unsafe-return": "off",
27
- "@typescript-eslint/no-unsafe-argument": "off",
28
- "import/extensions": "off",
29
- "@typescript-eslint/consistent-type-imports": "off",
30
- "@typescript-eslint/naming-convention": "off",
31
- "@typescript-eslint/no-floating-promises": "off",
32
- "import/no-extraneous-dependencies": "off",
33
- "@typescript-eslint/no-confusing-void-expression": "off",
34
- "@typescript-eslint/no-empty-function": "off",
35
- "promise/prefer-await-to-then": "off"
36
- }
37
- },
38
15
  "repository": {
39
16
  "type": "git",
40
17
  "url": "git+https://github.com/jaredwray/keyv.git"
@@ -58,13 +35,14 @@
58
35
  },
59
36
  "homepage": "https://github.com/jaredwray/keyv",
60
37
  "dependencies": {
61
- "@aws-sdk/client-dynamodb": "^3.835.0",
62
- "@aws-sdk/lib-dynamodb": "^3.835.0"
38
+ "@aws-sdk/client-dynamodb": "^3.883.0",
39
+ "@aws-sdk/lib-dynamodb": "^3.883.0"
63
40
  },
64
41
  "devDependencies": {
42
+ "@biomejs/biome": "^2.2.3",
65
43
  "vitest": "^3.2.4",
66
- "keyv": "^5.3.4",
67
- "@keyv/test-suite": "^2.0.9"
44
+ "@keyv/test-suite": "^2.1.1",
45
+ "keyv": "^5.5.1"
68
46
  },
69
47
  "tsd": {
70
48
  "directory": "test"
@@ -75,8 +53,8 @@
75
53
  ],
76
54
  "scripts": {
77
55
  "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
78
- "test": "xo --fix && vitest run --coverage",
79
- "test:ci": "xo && vitest --run --sequence.setupFiles=list --coverage",
56
+ "test": "biome check --write && vitest run --coverage",
57
+ "test:ci": "biome check && vitest --run --sequence.setupFiles=list --coverage",
80
58
  "clean": "rimraf ./node_modules ./coverage ./dist"
81
59
  }
82
60
  }