From 0799988f2737874801fd50da7502af511c9c898b Mon Sep 17 00:00:00 2001 From: mesqueeb Date: Tue, 4 Jun 2024 01:39:07 +0900 Subject: [PATCH] feat: implement storeSplit helper function --- docs/docs-main/write-data/index.md | 22 ++++++++++ package-lock.json | 42 +++++++++++++++++++ packages/core/package.json | 2 + .../src/moduleActions/handleWritePerStore.ts | 27 +++++++++--- .../test/internal/storeSplit.test.ts | 39 +++++++++++++++++ packages/utils/src/index.ts | 1 + packages/utils/src/internal/storeSplit.ts | 38 +++++++++++++++++ 7 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-firestore/test/internal/storeSplit.test.ts create mode 100644 packages/utils/src/internal/storeSplit.ts diff --git a/docs/docs-main/write-data/index.md b/docs/docs-main/write-data/index.md index 96186b0c..e010c0d2 100644 --- a/docs/docs-main/write-data/index.md +++ b/docs/docs-main/write-data/index.md @@ -192,3 +192,25 @@ for (const pkmn of newPokemon) { ``` Same goes for other methods to delete or modify data. + +## Pass Different Data to Different Stores + +You can import the `storeSplit` function and use it in any write call (insert, merge, replace, etc.) to write a different payload between your cache store vs your other stores. + +The function's return type will be whatever you pass to the `cache` key. The cache value will be written to your cache plugin's store, and for the other keys you can use any other stores names that you have set up in your magnetar instance. + +```ts +import { storeSplit } from '@magnetarjs/utils' + +magnetar + .collection('user') + .doc('1') + .merge({ + name: 'updated name', + // ... + dateUpdated: storeSplit({ + cache: new Date(), + remote: serverTimestamp(), + }), + }) +``` diff --git a/package-lock.json b/package-lock.json index b66d85a8..4c2f8fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8511,6 +8511,18 @@ "node": ">=12" } }, + "node_modules/map-anything": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/map-anything/-/map-anything-3.0.0.tgz", + "integrity": "sha512-+2gbec8vdbzysmOUqAL6HDiJIIBxAMtA/5ZIeFzKAXeHfa4m5HxSdPFHhc0QeZsjWDOq2hR5E0+U8rRAX5mNIg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -15736,8 +15748,10 @@ "dependencies": { "@magnetarjs/types": "*", "@magnetarjs/utils": "*", + "filter-anything": "^4.0.0", "getorset-anything": "^0.1.0", "is-what": "^5.0.0", + "map-anything": "^3.0.0", "merge-anything": "^5.1.7" }, "engines": { @@ -15747,6 +15761,34 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "packages/core/node_modules/filter-anything": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/filter-anything/-/filter-anything-4.0.0.tgz", + "integrity": "sha512-R4PCi8aFfnQGn3miRh8xMUlJh44Fj9lGcMu/bDOgyEbAAx2RwsN81HsnV+/FNN6LPIRzWSOJoOCKGXuPXmq3Aw==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8", + "ts-toolbelt": "^9.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "packages/core/node_modules/filter-anything/node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "packages/dev-vue3-firestore": { "version": "1.1.0", "dependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index 51ef8411..ffeed641 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,8 +25,10 @@ "dependencies": { "@magnetarjs/types": "*", "@magnetarjs/utils": "*", + "filter-anything": "^4.0.0", "getorset-anything": "^0.1.0", "is-what": "^5.0.0", + "map-anything": "^3.0.0", "merge-anything": "^5.1.7" }, "keywords": [ diff --git a/packages/core/src/moduleActions/handleWritePerStore.ts b/packages/core/src/moduleActions/handleWritePerStore.ts index 5927300b..07cc202c 100644 --- a/packages/core/src/moduleActions/handleWritePerStore.ts +++ b/packages/core/src/moduleActions/handleWritePerStore.ts @@ -14,8 +14,10 @@ import type { SyncBatch, WriteLock, } from '@magnetarjs/types' +import { isStoreSplit } from '@magnetarjs/utils' import { mapGetOrSet } from 'getorset-anything' -import { isFullArray, isFullString } from 'is-what' +import { isAnyObject, isFullArray, isFullString } from 'is-what' +import { mapObject } from 'map-anything' import { getEventNameFnsMap } from '../helpers/eventHelpers.js' import { getModifyPayloadFnsMap } from '../helpers/modifyPayload.js' import { getPluginModuleConfig } from '../helpers/moduleHelpers.js' @@ -105,9 +107,24 @@ export function handleWritePerStore( (globalConfig.executionOrder || {})['write'] || [] throwIfNoFnsToExecute(storesToExecute) + + const unwrapStoreSplits = (payloadChunk: any, storeName: string): any => { + return isStoreSplit(payloadChunk) + ? payloadChunk.storePayloadDic[storeName] + : isAnyObject(payloadChunk) + ? mapObject(payloadChunk, (value) => unwrapStoreSplits(value, storeName)) + : payloadChunk + } // update the payload + let storePayloadDic = storesToExecute.reduce<{ + [key: string]: any + cache?: any + }>((dic, storeName) => ({ ...dic, [storeName]: unwrapStoreSplits(payload, storeName) }), {}) + for (const modifyFn of modifyPayloadFnsMap[actionName]) { - payload = modifyFn(payload, docId) + storePayloadDic = mapObject(storePayloadDic, (payloadValue) => + modifyFn(payloadValue, docId), + ) } // create the abort mechanism @@ -152,7 +169,7 @@ export function handleWritePerStore( modulePath, pluginModuleConfig, pluginAction, - payload, // should always use the payload as passed originally for clarity + payload: storePayloadDic[storeName], // should always use the payload as passed originally for clarity actionConfig, eventNameFnsMap, onError, @@ -170,7 +187,7 @@ export function handleWritePerStore( const pluginModuleConfig = getPluginModuleConfig(moduleConfig, storeToRevert) if (pluginRevertAction) { await pluginRevertAction({ - payload, + payload: storePayloadDic[storeName], actionConfig, collectionPath, docId, @@ -181,7 +198,7 @@ export function handleWritePerStore( } // revert eventFns, handle and await each eventFn in sequence for (const fn of eventNameFnsMap.revert) { - await fn({ payload, result: resultFromPlugin, actionName, storeName, collectionPath, docId, path: modulePath, pluginModuleConfig }) // prettier-ignore + await fn({ payload: storePayloadDic[storeName], result: resultFromPlugin, actionName, storeName, collectionPath, docId, path: modulePath, pluginModuleConfig }) // prettier-ignore } } writeLock.resolve() diff --git a/packages/plugin-firestore/test/internal/storeSplit.test.ts b/packages/plugin-firestore/test/internal/storeSplit.test.ts new file mode 100644 index 00000000..5d070f2e --- /dev/null +++ b/packages/plugin-firestore/test/internal/storeSplit.test.ts @@ -0,0 +1,39 @@ +import { pokedex } from '@magnetarjs/test-utils' +import { storeSplit } from '@magnetarjs/utils' +import { merge } from 'merge-anything' +import { assert, test } from 'vitest' +import { createMagnetarInstance } from '../helpers/createMagnetarInstance.js' +import { firestoreDeepEqual } from '../helpers/firestoreDeepEqual.js' + +{ + const testName = 'write: merge (document) with storeSplit' + test(testName, async () => { + const { pokedexModule } = await createMagnetarInstance(testName, { + insertDocs: { 'pokedex/1': pokedex(1) }, + }) + + const doc = pokedexModule.doc('1') + assert.deepEqual(doc.data, pokedex(1)) + await firestoreDeepEqual(testName, 'pokedex/1', pokedex(1)) + + try { + await doc.merge( + { base: { HP: storeSplit({ cache: 9000, remote: '9000' }) } }, + { syncDebounceMs: 1 }, + ) + } catch (error) { + assert.fail(JSON.stringify(error)) + } + + const mergedResult = merge(pokedex(1), { base: { HP: 9000 } }) + + assert.deepEqual(pokedexModule.data.get('1'), mergedResult) + assert.deepEqual(doc.data, mergedResult) + + await firestoreDeepEqual(testName, 'pokedex/1', merge(pokedex(1), { base: { HP: '9000' } })) + + const fetchedDoc = await doc.fetch({ force: true }) + assert.deepEqual(fetchedDoc?.base.HP as any, '9000') + assert.deepEqual(doc.data?.base.HP as any, '9000') + }) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 9585a869..7c0428c2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -3,4 +3,5 @@ export * from './internal/dataHelpers.js' export * from './internal/debugHelpers.js' export * from './internal/parseValueForFilters.js' export * from './internal/pathHelpers.js' +export * from './internal/storeSplit.js' export * from './logger.js' diff --git a/packages/utils/src/internal/storeSplit.ts b/packages/utils/src/internal/storeSplit.ts new file mode 100644 index 00000000..229faf41 --- /dev/null +++ b/packages/utils/src/internal/storeSplit.ts @@ -0,0 +1,38 @@ +import { isAnyObject } from 'is-what' + +export const storeSplitSymbol = Symbol('storeSplit') + +/** + * A storeSplit function allows you to apply a different payload between your cache store vs your other stores. + * + * It will let TypeScript know that you're trying to apply the type of whatever you pass to the `cache` key, even though your other stores might receive other values. + * + * ### Example Use Case + * ```ts + * import { storeSplit } from '@magnetarjs/utils' + * + * magnetar.collection('user').doc('1').merge({ + * name: 'updated name', + * // ... + * dateUpdated: storeSplit({ + * cache: new Date(), + * remote: serverTimestamp(), + * }) + * }) + * ``` + */ +export function storeSplit( + payload: Payload, +): Payload['cache'] { + return { + storeSplitSymbol, + storePayloadDic: payload, + } as unknown as Payload['cache'] +} + +export function isStoreSplit(payload: unknown): payload is { + storeSplitSymbol: symbol + storePayloadDic: { cache: any; [key: string]: any } +} { + return isAnyObject(payload) && payload['storeSplitSymbol'] === storeSplitSymbol +}