Skip to content

Commit

Permalink
feat: CommonDao.patch (new method)
Browse files Browse the repository at this point in the history
Previous `patch` is renamed to `patchById`.
Their exact logic is jsdoc-documented and unit-tested now.
  • Loading branch information
kirillgroshkov committed Sep 11, 2023
1 parent 45ac021 commit 1e8f45c
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 71 deletions.
10 changes: 0 additions & 10 deletions src/commondao/__snapshots__/common.dao.test.ts.snap

This file was deleted.

62 changes: 57 additions & 5 deletions src/commondao/common.dao.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { mockTime, MOCK_TS_2018_06_21 } from '@naturalcycles/dev-lib/dist/testing'
import { ErrorMode, _omit, _range, _sortBy, pTry, pExpectedError } from '@naturalcycles/js-lib'
import {
ErrorMode,
_omit,
_range,
_sortBy,
pTry,
pExpectedError,
Saved,
} from '@naturalcycles/js-lib'
import {
AjvSchema,
AjvValidationError,
Expand Down Expand Up @@ -148,16 +156,60 @@ test('should propagate pipe errors', async () => {
expect(results).toEqual(items.filter(i => i.id !== 'id3'))
})

test('patch', async () => {
test('patchById', async () => {
const id = '123456'
const r = await dao.patch(id, {
const r = await dao.patchById(id, {
k1: 'k111',
})

const r2 = await dao.getById(id)
expect(r.id).toBe(id)
expect(r2).toEqual(r)
expect(r).toMatchSnapshot()
expect(r).toMatchInlineSnapshot(`
{
"created": 1529539200,
"id": "123456",
"k1": "k111",
"updated": 1529539200,
}
`)
})

test('patch', async () => {
const item: Saved<TestItemBM> = await dao.save({
id: 'id1',
k1: 'k1',
})

// Something changes the item in a different process
await dao.save({
...item,
k1: 'k2',
})

// item.k1 is still the same in this process
expect(item.k1).toBe('k1')

// We want to set item.k3 to 1
// Old-school careless way would be to just `item.k3 = 1` and save.
// But that would overwrite the `k1 = k2` change above.
// Instead, we apply a patch!
// Then we inspect item, and it should reflect the `k1 = k2` change.
const patchResult = await dao.patch(item, { k3: 5 })

// It tracks the same object
expect(patchResult).toBe(item)
expect(item.k3).toBe(5) // patch is applied
expect(item.k1).toBe('k2') // it pulled the change from DB (saved in the separate process)
expect(item).toMatchInlineSnapshot(`
{
"created": 1529539200,
"id": "id1",
"k1": "k2",
"k3": 5,
"updated": 1529539200,
}
`)
})

// todo: fix jest mock
Expand Down Expand Up @@ -209,7 +261,7 @@ test('modifications of immutable objects', async () => {
await expect(immutableDao.saveBatch(bms)).resolves.not.toThrow()

// Ensure Object can't be patched
await expect(immutableDao.patch(item1Saved.id, { k2: 'patchedk2' })).rejects.toThrow()
await expect(immutableDao.patchById(item1Saved.id, { k2: 'patchedk2' })).rejects.toThrow()

// Ensure object can't be deleted
await expect(immutableDao.deleteById(item1Saved.id)).rejects.toThrow()
Expand Down
57 changes: 32 additions & 25 deletions src/commondao/common.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
_filterNullishValues,
_filterUndefinedValues,
_isTruthy,
_objectAssignExact,
_passthroughPredicate,
_since,
_truncate,
Expand Down Expand Up @@ -844,25 +845,22 @@ export class CommonDao<
* 1.1 Creates the row (via this.create()) if it doesn't exist
* (this will cause a validation error if Patch has not enough data for the row to be valid).
* 2. Applies the patch on top of loaded data.
* 3. Saves (as fast as possible since the read) with the Patch applied.
* 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
*/
async patch(
async patchById(
id: ID,
patch: Partial<BM>,
opt: CommonDaoSaveBatchOptions<DBM> = {},
): Promise<Saved<BM>> {
const bm = await this.getById(id, opt)
let patched: Saved<BM>
const loaded = await this.getById(id, opt)

if (bm) {
patched = {
...bm,
...patch,
}
if (loaded) {
patched = { ...loaded, ...patch }

if (_deepJsonEquals(bm, patched)) {
if (_deepJsonEquals(loaded, patched)) {
// Skipping the save operation, as data is the same
return bm
return patched
}
} else {
patched = this.create({ ...patch, id }, opt)
Expand All @@ -871,29 +869,38 @@ export class CommonDao<
return await this.save(patched, opt)
}

async patchAsDBM(
id: ID,
patch: Partial<DBM>,
/**
* Same as patchById, but takes the whole object as input.
* This "whole object" is mutated with the patch and returned.
* Otherwise, similar behavior as patchById.
* It still loads the row from the DB.
*/
async patch(
bm: Saved<BM>,
patch: Partial<BM>,
opt: CommonDaoSaveBatchOptions<DBM> = {},
): Promise<DBM> {
const dbm = await this.getByIdAsDBM(id, opt)
let patched: DBM
): Promise<Saved<BM>> {
_assert(bm.id, 'patch argument object should have an id', {
bm,
})

if (dbm) {
patched = {
...dbm,
...patch,
}
const loaded = await this.getById(bm.id, opt)

if (_deepJsonEquals(dbm, patched)) {
if (loaded) {
Object.assign(loaded, patch)

if (_deepJsonEquals(loaded, bm)) {
// Skipping the save operation, as data is the same
return dbm
return bm
}

// Make `bm` exactly the same as `loaded`
_objectAssignExact(bm, loaded)
} else {
patched = this.create({ ...patch, id } as Partial<BM>, opt) as any as DBM
Object.assign(bm, patch)
}

return await this.saveAsDBM(patched, opt)
return await this.save(bm, opt)
}

async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM> {
Expand Down
62 changes: 31 additions & 31 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -516,10 +516,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"

"@eslint/js@8.48.0":
version "8.48.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==
"@eslint/js@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333"
integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==

"@hapi/hoek@^9.0.0":
version "9.3.0"
Expand All @@ -533,7 +533,7 @@
dependencies:
"@hapi/hoek" "^9.0.0"

"@humanwhocodes/config-array@^0.11.10":
"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
Expand Down Expand Up @@ -827,9 +827,9 @@
typescript "^5.0.2"

"@naturalcycles/dev-lib@^13.0.0":
version "13.42.2"
resolved "https://registry.yarnpkg.com/@naturalcycles/dev-lib/-/dev-lib-13.42.2.tgz#a96f52ebc65b7d58eab633bf93a25c1b8cc9437b"
integrity sha512-sgrLnZCsrQZjNMpO6qna0Lqi5iCfXLxrL6YTkd0dYPqcnWhrEfYxBP/NDXgG5uCYacx9m1VeKr7wsCuuAu3Jjw==
version "13.43.0"
resolved "https://registry.yarnpkg.com/@naturalcycles/dev-lib/-/dev-lib-13.43.0.tgz#c8069f828faea390a6c572340314f0388551412d"
integrity sha512-d2le0RoHjXaFPCWjaiUB3eeN4DkIS52movx8Z77hO8ovW5FS/eWw8tlkFQXGk7b9Elgd/7ghC4kucEmBNRiT8g==
dependencies:
"@commitlint/cli" "^17.0.0"
"@commitlint/config-conventional" "^17.0.0"
Expand Down Expand Up @@ -862,9 +862,9 @@
yargs "^17.0.0"

"@naturalcycles/js-lib@^14.0.0", "@naturalcycles/js-lib@^14.116.0":
version "14.184.1"
resolved "https://registry.yarnpkg.com/@naturalcycles/js-lib/-/js-lib-14.184.1.tgz#499505a29fd3dc04e11b0159b730fd8824ddfe7b"
integrity sha512-aGvR8K6kjPIudGw9PaxWxLRUzDhgjzFMnLUWIedVUl+DzNfckgxwhEx5/Qm62+ZHDCn6KdNf3NdNiXUDHCXwsA==
version "14.185.0"
resolved "https://registry.yarnpkg.com/@naturalcycles/js-lib/-/js-lib-14.185.0.tgz#74e38d37395fbd6edbde0c5555c64da7f7a5b777"
integrity sha512-X2F+KcujHMkAsrJJvKcKp+2c30lyTV+vw68GIgNZPQGylilnpsXK2hjzTyh79zeQ9/AZB7oPgVoZHzD4hMxSLg==
dependencies:
tslib "^2.0.0"
zod "^3.20.2"
Expand Down Expand Up @@ -1075,9 +1075,9 @@
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==

"@types/node@*", "@types/node@^20.1.0", "@types/node@^20.2.1":
version "20.5.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a"
integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==
version "20.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16"
integrity sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==

"@types/[email protected]":
version "20.4.7"
Expand Down Expand Up @@ -1739,9 +1739,9 @@ camelcase@^6.2.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==

caniuse-lite@^1.0.30001517:
version "1.0.30001529"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001529.tgz#c1f2a411e85fdaace4b1560e1bad078b00ac3181"
integrity sha512-n2pUQYGAkrLG4QYj2desAh+NqsJpHbNmVZz87imptDdxLAtjxary7Df/psdfyDGmskJK/9Dt9cPnx5RZ3CU4Og==
version "1.0.30001532"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz#c6a4d5d2da6d2b967f0ee5e12e7f680db6ad2fca"
integrity sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==

[email protected]:
version "5.3.0"
Expand Down Expand Up @@ -1981,9 +1981,9 @@ cosmiconfig-typescript-loader@^4.0.0:
integrity sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==

cosmiconfig@^8.0.0:
version "8.3.4"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.4.tgz#ee1356e7f24e248a6bb34ec5d438c3dcebeb410c"
integrity sha512-SF+2P8+o/PTV05rgsAjDzL4OFdVXAulSfC/L19VaeVT7+tpOOSscCt2QLxDZ+CLxF2WOiq6y1K5asvs8qUJT/Q==
version "8.3.5"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.5.tgz#3b3897ddd042d022d5a207d4c8832e54f5301977"
integrity sha512-A5Xry3xfS96wy2qbiLkQLAg4JUrR2wvfybxj6yqLmrUfMAvhS3MZxIP2oQn0grgYIvJqzpeTEWu4vK0t+12NNw==
dependencies:
import-fresh "^3.3.0"
js-yaml "^4.1.0"
Expand Down Expand Up @@ -2293,9 +2293,9 @@ [email protected]:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==

electron-to-chromium@^1.4.477:
version "1.4.512"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.512.tgz#f6c14d4d2ddacf064f1de36dbd3f6a469821a7ee"
integrity sha512-1W8wRbYlQE4ph7eoj3TJ+uqwO6+xvAE/L+KGU7WTQQvX3tnSIGZAb90MTsMoJqzntamiwJhBAj4WZmygXhsOUg==
version "1.4.513"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz#41a50bf749aa7d8058ffbf7a131fc3327a7b1675"
integrity sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==

emittery@^0.13.1:
version "0.13.1"
Expand Down Expand Up @@ -2471,9 +2471,9 @@ eslint-plugin-jest@^27.0.1:
"@typescript-eslint/utils" "^5.10.0"

eslint-plugin-jsdoc@^46.0.0:
version "46.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.5.1.tgz#79be44c9f7b3bbc52917cae12f356979b7d58e8a"
integrity sha512-CPbvKprmEuJYoxMj5g8gXfPqUGgcqMM6jpH06Kp4pn5Uy5MrPkFKzoD7UFp2E4RBzfXbJz1+TeuEivwFVMkXBg==
version "46.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.6.0.tgz#513e41911a3081a15fe6a94490002e8d8f7b71d9"
integrity sha512-T/1gzsvnX45qABzyPEonEhFDttkTn7Igm/X89TXIkTLBOsNl2GYtyBqQPZGXZZ8J5VBzEhiCMvI2P2kXX4dnFw==
dependencies:
"@es-joy/jsdoccomment" "~0.40.1"
are-docs-informative "^0.0.2"
Expand Down Expand Up @@ -2553,15 +2553,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==

eslint@^8.0.0:
version "8.48.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155"
integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==
version "8.49.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42"
integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.2"
"@eslint/js" "8.48.0"
"@humanwhocodes/config-array" "^0.11.10"
"@eslint/js" "8.49.0"
"@humanwhocodes/config-array" "^0.11.11"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.12.4"
Expand Down

0 comments on commit 1e8f45c

Please sign in to comment.