Skip to content

Commit

Permalink
feat: add option to return item on prop updates (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
timkinnane authored Sep 30, 2021
1 parent e947847 commit 118d326
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 44 deletions.
125 changes: 87 additions & 38 deletions src/dynaModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,54 @@ describe('providers > dynamo > dynaModel', () => {
})
describe('makeInsertProperty', () => {
describe('with root property', () => {
it('without existing item, function creates item and property, sets meta, returns value', async () => {
const inserted = await makeInsertProperty('testData')(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ foo: true })
expect((found.Item as TestProps))
.toEqual({ ...Key, testData: { foo: true }, ...expectMeta })
})
it('without existing prop, function sets property and returns value', async () => {
await ddb.put({ TableName, Item }).promise()
await expect(makeInsertProperty('testData')(Key, { foo: true }))
.resolves.toEqual({ foo: true })
describe('without options (attribute mode)', () => {
it('without existing item, function creates item and property, sets meta, returns value', async () => {
const inserted = await makeInsertProperty('testData')(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ foo: true })
expect((found.Item as TestProps))
.toEqual({ ...Key, testData: { foo: true }, ...expectMeta })
})
it('without existing prop, function sets property and returns value', async () => {
await ddb.put({ TableName, Item }).promise()
await expect(makeInsertProperty('testData')(Key, { foo: true }))
.resolves.toEqual({ foo: true })
})
it('with existing prop, function does nothing, returns existing', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
const inserted = await makeInsertProperty('testData')(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ foo: false })
expect((found.Item as TestProps).testData)
.toEqual({ foo: false })
})
})
it('with existing prop, function does nothing, returns existing', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
const inserted = await makeInsertProperty('testData')(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ foo: false })
expect((found.Item as TestProps).testData)
.toEqual({ foo: false })
describe('with options (item mode)', () => {
it('without existing item, function creates item and property, sets meta, returns item', async () => {
const inserted = await makeInsertProperty('testData', { projection: 'Item' })(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ ...Key, testData: { foo: true }, ...expectMeta })
expect((found.Item as TestProps))
.toEqual({ ...Key, testData: { foo: true }, ...expectMeta })
})
it('without existing prop, function sets property (and meta) and returns item', async () => {
await ddb.put({ TableName, Item }).promise()
const inserted = await makeInsertProperty('testData', { projection: 'Item' })(Key, { foo: true })
expect(inserted as TestProps)
.toEqual({ ...Item, testData: { foo: true }, ...expectMeta })
})
it('with existing prop, function does nothing, returns existing item', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
const inserted = await makeInsertProperty('testData', { projection: 'Item' })(Key, { foo: true })
const found = await ddb.get({ TableName, Key }).promise()
expect(inserted)
.toEqual({ ...Item, testData: { foo: false }, ...expectMeta })
expect((found.Item as TestProps).testData)
.toEqual({ foo: false })
})
})
})
describe('with nested property', () => {
Expand Down Expand Up @@ -167,24 +194,6 @@ describe('providers > dynamo > dynaModel', () => {
})
})
describe('makeUpdateProperty', () => {
it('function assigns a property, returning changes', async () => {
await expect(makeUpdateProperty('testCount')(Key, 99))
.resolves.toEqual(99)
})
it('function assigns nested property, returning changes', async () => {
await expect(makeUpdateProperty('testData.foo')(Key, true))
.resolves.toEqual(true)
})
it('function updates a property, returning changes', async () => {
await ddb.put({ TableName, Item: { ...Item, testCount: 1 } }).promise()
await expect(makeUpdateProperty('testCount')(Key, 99))
.resolves.toEqual(99)
})
it('function updates nested property, returning changes', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
await expect(makeUpdateProperty('testData.foo')(Key, true))
.resolves.toEqual(true)
})
it('functions assign to objects without effecting other properties', async () => {
await ddb.put({ TableName, Item }).promise()
await makeUpdateProperty('testData.foo')(Key, false)
Expand All @@ -196,6 +205,46 @@ describe('providers > dynamo > dynaModel', () => {
bar: false
})
})
describe('without options (attribute mode)', () => {
it('function assigns a property, returning changes', async () => {
await expect(makeUpdateProperty('testCount')(Key, 99))
.resolves.toEqual(99)
})
it('function assigns nested property, returning changes', async () => {
await expect(makeUpdateProperty('testData.foo')(Key, true))
.resolves.toEqual(true)
})
it('function updates a property, returning changes', async () => {
await ddb.put({ TableName, Item: { ...Item, testCount: 1 } }).promise()
await expect(makeUpdateProperty('testCount')(Key, 99))
.resolves.toEqual(99)
})
it('function updates nested property, returning changes', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
await expect(makeUpdateProperty('testData.foo')(Key, true))
.resolves.toEqual(true)
})
})
describe('with options (item mode)', () => {
it('function assigns a property, returning item', async () => {
await expect(makeUpdateProperty('testCount', { projection: 'Item' })(Key, 99))
.resolves.toEqual({ ...Key, testCount: 99, ...expectMeta })
})
it('function assigns nested property, returning item', async () => {
await expect(makeUpdateProperty('testData.foo', { projection: 'Item' })(Key, true))
.resolves.toEqual({ ...Key, testData: { foo: true }, ...expectMeta })
})
it('function updates a property, returning item', async () => {
await ddb.put({ TableName, Item: { ...Item, testCount: 1 } }).promise()
await expect(makeUpdateProperty('testCount', { projection: 'Item' })(Key, 99))
.resolves.toEqual({ ...Item, testCount: 99, ...expectMeta })
})
it('function updates nested property, returning item', async () => {
await ddb.put({ TableName, Item: { ...Item, testData: { foo: false } } }).promise()
await expect(makeUpdateProperty('testData.foo', { projection: 'Item' })(Key, true))
.resolves.toEqual({ ...Item, testData: { foo: true }, ...expectMeta })
})
})
})
/** @todo Below for manual IDE type checking only. Need better tests for types; DTS-Jest? */
describe.skip('return types', () => {
Expand Down
27 changes: 22 additions & 5 deletions src/dynaModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export type NonPartial<T> = { [K in keyof Required<T>]: T[K] };
*/
type Item<Props, Key extends DocumentClient.Key> = Key & Props & MetaProps

/** Options for update/insert methods. */
type Options = {
/** Return the touched attributes, or the whole item */
projection?: 'Item' | 'Attributes'
}

/** Default options to merge with given. */
const defaults: Options = {
projection: 'Attributes'
}

/**
* Provides a suite of functions to construct a typed DynamoDB data model CRUD interface.
* @param ddb An instantiated DDB client so configuration is delegated to consumer
Expand Down Expand Up @@ -134,8 +145,9 @@ export const dynaModel = <
* const updateUserName = set<UserData>('name')
* await updateUserName('a_user_id', 'a_user_name')
*/
function makeUpdateProperty <P extends string> (path: PathOf<Props, P>) {
return async <T extends AtPath<Props, P>>(Key: HashKeys, valueAtPath: T): Promise<T> => {
function makeUpdateProperty <P extends string> (path: PathOf<Props, P>, options: Options = {}) {
const { projection } = { ...defaults, ...options }
return async <T extends AtPath<Props, P>>(Key: HashKeys, valueAtPath: T) => {
const paths = path.split('.')
const value = nestedValue(paths, valueAtPath)
for (const index of paths.keys()) {
Expand All @@ -159,7 +171,9 @@ export const dynaModel = <
])
}).promise().catch(handleFailedCondition)
if (result?.Attributes) {
return atPath(result.Attributes, path) as Promise<T>
return projection === 'Attributes'
? atPath(result.Attributes, path) as T
: result.Attributes as Item<Props, HashKeys>
}
}
throw new Error(`No attributes returned from update to ${path} on ${TableName}`)
Expand All @@ -175,8 +189,9 @@ export const dynaModel = <
* const updateUserName = set<UserData>('name')
* await updateUserName('a_user_id', 'a_user_name')
*/
function makeInsertProperty <P extends string> (path: PathOf<Props, P>) {
function makeInsertProperty <P extends string> (path: PathOf<Props, P>, options: Options = {}) {
return async <T extends AtPath<Props, P>>(Key: HashKeys, valueAtPath: T) => {
const { projection } = { ...defaults, ...options }
const paths = path.split('.')
const value = nestedValue(paths, valueAtPath)
for (const index of paths.keys()) {
Expand All @@ -200,7 +215,9 @@ export const dynaModel = <
])
}).promise().catch(handleFailedCondition)
if (result && result.Attributes) {
return atPath(result.Attributes, path) as Promise<T>
return projection === 'Attributes'
? atPath(result.Attributes, path) as T
: result.Attributes as Item<Props, HashKeys>
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"forceConsistentCasingInFileNames": true,
"preserveConstEnums": true,
"skipLibCheck": true,
"typeRoots": ["node_modules/@types"]
"typeRoots": ["node_modules/@types", "node_modules/@jest/types"]
}
}

0 comments on commit 118d326

Please sign in to comment.