Skip to content

Commit

Permalink
feat: implement storeSplit helper function
Browse files Browse the repository at this point in the history
  • Loading branch information
mesqueeb committed Jun 3, 2024
1 parent 79439ff commit 0799988
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 5 deletions.
22 changes: 22 additions & 0 deletions docs/docs-main/write-data/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
})
```
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
27 changes: 22 additions & 5 deletions packages/core/src/moduleActions/handleWritePerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -170,7 +187,7 @@ export function handleWritePerStore(
const pluginModuleConfig = getPluginModuleConfig(moduleConfig, storeToRevert)
if (pluginRevertAction) {
await pluginRevertAction({
payload,
payload: storePayloadDic[storeName],
actionConfig,
collectionPath,
docId,
Expand All @@ -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()
Expand Down
39 changes: 39 additions & 0 deletions packages/plugin-firestore/test/internal/storeSplit.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
38 changes: 38 additions & 0 deletions packages/utils/src/internal/storeSplit.ts
Original file line number Diff line number Diff line change
@@ -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 extends { cache: any; [key: string]: any }>(
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
}

0 comments on commit 0799988

Please sign in to comment.