Skip to content

Commit f7b222d

Browse files
committed
BREAKING CHANGE - updates to Store setup
Breaking changes: * Now only one DynamoDB wrapper / client per store instance * A whole bunch of changes to store setup and config Also documentation for setup The driver for this was that configuration with multiple DynamoDB clients was messy, and I don't think is that useful. For different clients the user can just instantiate different instances of the entity store.
1 parent e8dca89 commit f7b222d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+757
-496
lines changed

README.md

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,18 @@ We are using a ["Single Table Design"](https://www.alexdebrie.com/posts/dynamodb
7575

7676
Now we add / install Entity Store in the usual way [from NPM](https://www.npmjs.com/package/@symphoniacloud/dynamodb-entity-store) , e.g.
7777

78-
```% npm install @symphoniacloud/dynamodb-entity-store```
78+
```
79+
% npm install @symphoniacloud/dynamodb-entity-store
80+
```
7981

8082
Let's assume that our DynamoDB Table is named `AnimalsTable`.
8183

8284
We can create an entity store by using
8385
the [`createStore`](https://symphoniacloud.github.io/dynamodb-entity-store/functions/createStore.html)
84-
and [`createStandardSingleTableStoreConfig`](https://symphoniacloud.github.io/dynamodb-entity-store/functions/createStandardSingleTableStoreConfig.html) functions:
86+
and [`createStandardSingleTableConfig`](https://symphoniacloud.github.io/dynamodb-entity-store/functions/createStandardSingleTableConfig.html) functions:
8587

8688
```typescript
87-
const entityStore = createStore(createStandardSingleTableStoreConfig('AnimalsTable'))
89+
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'))
8890
```
8991

9092
`entityStore` is an object that implements
@@ -116,7 +118,7 @@ We only need to create this object **once per type** of entity in our applicatio
116118
* **Optional:** express how to convert an object to a DynamoDB record ("formatting")
117119
* **Optional:** Create Global Secondary Index (GSI) key values
118120

119-
> A complete discussion of _Entities_ is available in [the manual, here](./documentation/1-Entities.md).
121+
> A complete discussion of _Entities_ is available in [the manual, here](./documentation/Entities.md).
120122
121123
We can now call `.for(...)` on our entity store. This returns an object that implements [`SingleEntityOperations`](https://symphoniacloud.github.io/dynamodb-entity-store/interfaces/SingleEntityOperations.html) - **this is the object that you'll likely work with most when using this library**.
122124

@@ -223,9 +225,8 @@ When you're working on setting up your entities and queries you'll often want to
223225
doing. You can do this by turning on logging:
224226

225227
```typescript
226-
const config = createStandardSingleTableStoreConfig('AnimalsTable')
227-
config.store.logger = consoleLogger
228-
const entityStore = createStore(config)
228+
const config = createStandardSingleTableConfig('AnimalsTable')
229+
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'), { logger: consoleLogger })
229230
```
230231

231232
With this turned on we can see the output from our last query:
@@ -313,7 +314,7 @@ Write operations are no different than before - Entity Store handles generating
313314
generator functions. So if we have the following...
314315

315316
```typescript
316-
const entityStore = createStore(createStandardSingleTableStoreConfig('AnimalsTable'))
317+
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'))
317318
const chickenStore = entityStore.for(CHICKEN_ENTITY)
318319

319320
await chickenStore.put({ breed: 'sussex', name: 'ginger', dateOfBirth: '2021-07-01', coop: 'bristol' })
@@ -403,16 +404,11 @@ We create the entity store using a custom configuration:
403404

404405
```typescript
405406
const entityStore = createStore(
406-
createSingleTableConfiguration({
407-
tableName: 'FarmTable',
408-
metaAttributeNames: { pk: 'Name' }
409-
})
407+
createMinimumSingleTableConfig('FarmTable', { pk: 'Name' })
410408
)
411409
```
412410

413-
We're only using one table (here "single table" just means one table, rather than the "standard" configuration), and we
414-
override
415-
the `metaAttributeNames` settings to only store the partition key, with the correct attribute name.
411+
> See [_Setup_ in the manual](documentation/Setup.md) for more on custom table configuration.
416412
417413
The `Entity` this time is a bit more complicated:
418414

@@ -496,16 +492,13 @@ results in:
496492
Error: Scan operations are disabled for this store
497493
```
498494

499-
However, we can change our store configuration to be the following:
495+
However, we can change our table configuration to be the following:
500496

501497
```typescript
502-
const entityStore = createStore(
503-
createSingleTableConfiguration({
504-
tableName: 'FarmTable',
505-
metaAttributeNames: { pk: 'Name' },
506-
allowScans: true
507-
})
508-
)
498+
const entityStore = createStore({
499+
...createMinimumSingleTableConfig('FarmTable', { pk: 'Name' }),
500+
allowScans: true
501+
})
509502
```
510503

511504
and now we can run our scan:

documentation/2-Setup.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

documentation/1-Entities.md renamed to documentation/Entities.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ function pk({ name }: Pick<Farm, 'name'>) {
136136

137137
### `.convertToDynamoFormat()` (optional)
138138

139-
`convertToDynamoFormat()` is an optional function you may choose to implement in order to change how DynamoDB Entity Store writes an object to DynamoDB during `put` operations. Since DynamoDB Entity Store uses the [AWS Document Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) library under the covers, this is more about choosing which fields to save, and any field-level modification, rather than lower-level "marshalling". If you need to change marshalling options at the AWS library level please refer to the [Setup chapter](2-Setup.md).
139+
`convertToDynamoFormat()` is an optional function you may choose to implement in order to change how DynamoDB Entity Store writes an object to DynamoDB during `put` operations.
140+
Since DynamoDB Entity Store uses the [AWS Document Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) library under the covers, this is more about choosing which fields to save, and any field-level modification, rather than lower-level "marshalling".
141+
If you need to change marshalling options at the AWS library level please refer to the [Setup chapter](Setup.md).
140142

141143
By default DynamoDB Entity Store will store all the fields of an object, unmanipulated, using the field names of the object. E.g. going back to our `Sheep` example, let's say we're writing the following object:
142144

@@ -214,7 +216,7 @@ The `gsis` field defines _generator_ functions for all of the Global Secondary I
214216
215217
The type of `gsis` is `Record<string, GsiGenerators>`, a map from a GSI identifier to a GSI PK generator, and optionally a GSI SK generator.
216218
217-
The **GSI identifier** will typically be the same as, or similar to, the name of your actual DynamoDB GSI. The mapping from _Entity_ GSI ID to DynamoDB GSI Name is configured in [Table Setup](2-Setup.md), but as an example the "standard" configuration uses `gsi` as the _Entity_ GSI ID, and `GSI` for the corresponding index name.
219+
The **GSI identifier** will typically be the same as, or similar to, the name of your actual DynamoDB GSI. The mapping from _Entity_ GSI ID to DynamoDB GSI Name is configured in [Table Setup](Setup.md), but as an example the "standard" configuration uses `gsi` as the _Entity_ GSI ID, and `GSI` for the corresponding index name.
218220
219221
If you understand the table `pk()` and `sk()` generators then you'll understand the GSI Generators too. See the [example in the project README](https://github.com/symphoniacloud/dynamodb-entity-store/blob/main/README.md#example-2-adding-a-global-secondary-index) for an example.
220222

documentation/Setup.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Chapter 2 - Setup
2+
3+
## Library and Module
4+
5+
[_DynamoDB Entity Store_](https://github.com/symphoniacloud/dynamodb-entity-store) is available [from NPM](https://www.npmjs.com/package/@symphoniacloud/dynamodb-entity-store), and can be installed in the usual way, e.g.:
6+
7+
```
8+
% npm install @symphoniacloud/dynamodb-entity-store
9+
```
10+
11+
The library is provided in both CommonJS and ESModule form. All entrypoints are available from the root _index.js_ file.
12+
13+
> _I tried using package.json [exports](https://nodejs.org/api/packages.html#exports) but IDE support seems flakey, so I've reverted for now to just supporting a root "barrel" file_
14+
15+
## Instantiating Entity Store
16+
17+
The main entry point for Entity Store is the function [`createStore(config)`](https://symphoniacloud.github.io/dynamodb-entity-store/functions/createStore.html). This function returns an instance of the [`AllEntitiesStore`](https://symphoniacloud.github.io/dynamodb-entity-store/interfaces/AllEntitiesStore.html) type which you can use to perform operations on your DynamoDB table(s).
18+
19+
`createStore(config)` takes one required argument and one optional argument:
20+
* `tablesConfig` defines the names and configuration of all the tables you want to access through an instance of the Store.
21+
* `context` provides implementations of various behaviors. If you don't use it then defaults are used.
22+
23+
For some scenarios using all the default values of DynamoDB Entity Store will be sufficient. In such a case you can instantiate your store as follows:
24+
25+
```typescript
26+
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable')) // "AnimalsTable" is an example
27+
```
28+
29+
Typically though you'll need to change behavior in some form. I'll start with describing how to update `context`.
30+
31+
### Overriding `context`
32+
33+
`context` is an object of type [`TableBackedStoreContext`](https://symphoniacloud.github.io/dynamodb-entity-store/interfaces/TableBackedStoreContext.html), defined as follows:
34+
35+
```typescript
36+
{
37+
logger: EntityStoreLogger
38+
dynamoDB: DynamoDBInterface
39+
clock: Clock
40+
}
41+
```
42+
43+
If you don't specify a context when calling `createStore()` then the default values are used, as follows:
44+
45+
* `logger` : No-op logger (Don't log)
46+
* `dynamoDB` : Wrapper using default DynamoDB document client. (See below for details)
47+
* `clock` : Real clock based on system time (it can be useful to override this in tests)
48+
49+
Use the [`createStoreContext()`](https://symphoniacloud.github.io/dynamodb-entity-store/functions/createStoreContext.html) function to create a context with different values.
50+
With no arguments it provides precisely the same default values, but you can provide overrides as necessary. Here are a few such scenarios.
51+
52+
#### Overriding the DynamoDB Document Client or DynamoDB wrapper
53+
54+
By default DynamoDB Entity Store uses the default [DynamoDB Document Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) object, which uses the AWS account and region in the current context (e.g. from environment variables) and default marshalling / unmarshalling (see the official [AWS Documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/) for more details).
55+
56+
If you want to override any of this behavior you can provide your own Document Client object as the second argument to `createStoreContext()`.
57+
58+
For example to override the region you might call the following:
59+
60+
```typescript
61+
const storeContext = createStoreContext({}, DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'us-east-1' })))
62+
```
63+
64+
DynamoDB Entity Store uses a "wrapper" around the Document Client, which is the `dynamoDB` property on the Store Context. You can also override this, but typically you'd only do so for unit / in-process tests.
65+
66+
#### Specifying a logger
67+
68+
DynamoDB Entity Store will log various behavior at **debug level**. You can override the library's logger when calling `createStoreContext()`, e.g. `createStoreContext({ logger: consoleLogger })`.
69+
70+
The default implementation is a "no-op" logger, i.e. don't actually log anywhere.
71+
However you can instead use the [`consoleLogger`](https://symphoniacloud.github.io/dynamodb-entity-store/variables/consoleLogger.html), or you can provide your own implementation of [`EntityStoreLogger`](https://symphoniacloud.github.io/dynamodb-entity-store/interfaces/EntityStoreLogger.html).
72+
E.g. here's an implementation that uses the [AWS Powertools Logger](https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/) :
73+
74+
```typescript
75+
// Create an implementation of DynamoDB Entity Store Logger using an underlying AWS Powertools Logger
76+
function createPowertoolsEntityStoreLogger(logger: Logger): EntityStoreLogger {
77+
return {
78+
getLevelName() {
79+
return logger.getLevelName()
80+
},
81+
debug(input: LogItemMessage, ...extraInput) {
82+
logger.debug(input, ...extraInput)
83+
}
84+
}
85+
}
86+
```
87+
88+
#### Overriding the Clock
89+
90+
DynamoDB Entity Store uses a clock when generating the Last Updated field on items. By default this is the system clock, but you
91+
can override this - typically you'd only want to do so in tests. For an example see [`FakeClock`](https://github.com/symphoniacloud/dynamodb-entity-store/blob/main/test/unit/testSupportCode/fakes/fakeClock.ts) in the project's own test code.
92+
93+
### Configuring Tables
94+
95+
DynamoDB entity store can use one or more tables when performing operations.
96+
You specify your entire table configuration as the first argument of `createStore(config)`.
97+
I'll first explain how to configure Entity Store when using one table, and then will expand this to why and how you might want to configure multiple tables.
98+
99+
#### Single-table Configuration
100+
101+
A single table is configured using the `TableConfig` interface:
102+
103+
```typescript
104+
export interface TableConfig {
105+
tableName: string
106+
metaAttributeNames: {
107+
pk: string
108+
sk?: string
109+
gsisById?: Record<string, { pk: string; sk?: string }>
110+
ttl?: string
111+
entityType?: string
112+
lastUpdated?: string
113+
}
114+
allowScans?: boolean
115+
gsiNames?: Record<string, string>
116+
}
117+
```
118+
119+
If you want you can "hand-roll" this object, however there are support functions in [_setupSupport.ts_](../src/lib/support/setupSupport.ts) to help out.
120+
121+
For example, say you want to use a "standard single table" configuration. To create one of these you can call `createStandardSingleTableConfig()`, just passing your underlying table name. The resulting configuration will be as follows:
122+
123+
```typescript
124+
{
125+
tableName: 'testTable',
126+
allowScans: false,
127+
metaAttributeNames: {
128+
pk: 'PK',
129+
sk: 'SK',
130+
ttl: 'ttl',
131+
entityType: '_et',
132+
lastUpdated: '_lastUpdated',
133+
gsisById: {
134+
gsi: {
135+
pk: 'GSIPK',
136+
sk: 'GSISK'
137+
}
138+
}
139+
},
140+
gsiNames: {
141+
gsi: 'GSI'
142+
}
143+
}
144+
```
145+
146+
This configuration is valid when:
147+
148+
* Your table partition key attribute is named `PK`
149+
* You have a table sort key and the attribute is named `SK`
150+
* You have one GSI (Global Secondary Index) which is named `GSI`. It has a string partition key named `GSIPK` and a string sort key named `GSISK`. You reference this in entities using the "logical" ID `gsi`.
151+
* You want to automatically create `_et` and `_lastUpdated` attributes for each item.
152+
* If you specify a TTL (Time-To-Live) value when writing an object then it will be stored in an attributed named `ttl`
153+
* You don't want to allow scans
154+
155+
If any of your configuration is different from this you can do the following:
156+
157+
* Use the `createMinimumSingleTableConfig()` function, providing the table name and meta attribute names, and then add any other necessary properties
158+
* Use the `createStandardSingleTableConfig()` function above, and replace properties
159+
* Build your own implementation of `TableConfig`
160+
161+
The particular behaviors of this configuration will be explained in later parts of this manual.
162+
163+
#### When to use multi-table configuration
164+
165+
Some projects will use multiple DynamoDB tables. While I'm a fan of DynamoDB "single table design", I think there's often a place to use different tables for different operational reasons. And sometimes you'll be working in a project that doesn't use single table design.
166+
167+
When your project has multiple tables you can choose one of the following:
168+
169+
* Create an `AllEntitiesStore` per table, each store using single-table configuration
170+
* Create one or several `AllEntitiesStore`(s) that have a multi-table configuration
171+
172+
The way that multi-table configuration works with DynamoDB Entity Store is that each entity can only be stored in one table, and in the setup configuration each table includes the list of entities contained within it.
173+
To be clear - **one table can store multiple entities, but each entity can only be stored in one table**, for each Entity Store instance.
174+
175+
So if you want to store the same entity in multiple tables that immediately drives to using multiple `AllEntitiesStore` instances.
176+
177+
DynamoDB Entity Store uses one underlying Document Client per instance.
178+
Another reason to use multiple instances therefore is if the different tables have different DynamoDB document client configuration. For example:
179+
180+
* Different tables are in different accounts / regions / have different credentials
181+
* Different tables use different marshalling / unmarshalling options
182+
183+
However it's often the case that the constraint of one-table-per-entity, and common-document-client-per-table, is absolutely fine, and in such a case using a multi-table configuration of DynamoDB Entity Store can be used. This has the following advantages:
184+
185+
* Less code / state in your application
186+
* Ability to perform transactions across multiple entities in different tables
187+
188+
#### How to use multi-table configuration
189+
190+
To use a multi-table configuration call `createStore(config)` just as you would do for single-table, but
191+
the config object needs to be of type `MultiTableConfig`, as follows:
192+
193+
```typescript
194+
export interface MultiTableConfig {
195+
entityTables: MultiEntityTableConfig[]
196+
defaultTableName?: string
197+
}
198+
199+
export interface MultiEntityTableConfig extends TableConfig {
200+
entityTypes?: string[]
201+
}
202+
```
203+
204+
In other words a multi-table config consists of:
205+
206+
* An array of regular `TableConfig` objects, each having the addition of array of entity type names stored in the table
207+
* An optional default table name
208+
209+
The entity type names must be precisely the same as those specified in the `type` field of the _Entities_ you'll be using when performing operations.
210+
When you make calls to the operations functions in Entity Store the library will first find the table configuration used for that _Entity_.
211+
212+
The `defaultTableName` property is useful if you have a situation where _most_ entities are in one table, but you have a few "special cases" of other entities being in different tables.
213+
214+
You have a few options of how to create a `MultiTableConfig` object:
215+
216+
* Use the `createStandardMultiTableConfig()` function if all of your tables use the same "standard" configuration described earlier
217+
* Build your own configuration, optionally using the other support functions in [_setupSupport.ts_](../src/lib/support/setupSupport.ts).
File renamed without changes.

documentation/manual.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
For an overview of using DynamoDB Entity Store, please see the [README](../README.md).
44

5-
1. [Entities](1-Entities.md)
6-
2. [Setup](2-Setup.md)
7-
3. [Simple Usage](3-SimpleUsage.md)
5+
1. [Entities](Entities.md)
6+
2. [Setup](Setup.md)
7+
3. [Simple Usage](SimpleUsage.md)

examples/src/example1Sheep.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
createEntity,
3-
createStandardSingleTableStoreConfig,
3+
createStandardSingleTableConfig,
44
createStore,
55
DynamoDBValues,
66
rangeWhereSkBetween
@@ -29,7 +29,7 @@ export const SHEEP_ENTITY = createEntity(
2929

3030
async function run() {
3131
// Create entity store using default configuration
32-
const config = createStandardSingleTableStoreConfig('AnimalsTable')
32+
const config = createStandardSingleTableConfig('AnimalsTable')
3333
// config.store.logger = consoleLogger
3434
const entityStore = createStore(config)
3535

examples/src/example2Chickens.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
createStandardSingleTableStoreConfig,
2+
createStandardSingleTableConfig,
33
createStore,
44
Entity,
55
rangeWhereSkBeginsWith,
@@ -68,7 +68,7 @@ export function gsiBreed(breed: string) {
6868

6969
async function run() {
7070
// Create entity store using default configuration
71-
const entityStore = createStore(createStandardSingleTableStoreConfig('AnimalsTable'))
71+
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'))
7272
const chickenStore = entityStore.for(CHICKEN_ENTITY)
7373

7474
await chickenStore.put({ breed: 'sussex', name: 'ginger', dateOfBirth: '2021-07-01', coop: 'bristol' })

0 commit comments

Comments
 (0)