Automatically refresh data when related data changes

This commit is contained in:
Andrew Kingston 2021-02-05 16:16:41 +00:00
parent 7c3ccf69f9
commit fe00c66700
6 changed files with 161 additions and 23 deletions

View File

@ -1,4 +1,4 @@
import { notificationStore } from "../store/notification" import { notificationStore, datasourceStore } from "../store"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
@ -6,6 +6,9 @@ import { fetchTableDefinition } from "./tables"
* Fetches data about a certain row in a table. * Fetches data about a certain row in a table.
*/ */
export const fetchRow = async ({ tableId, rowId }) => { export const fetchRow = async ({ tableId, rowId }) => {
if (!tableId || !rowId) {
return
}
const row = await API.get({ const row = await API.get({
url: `/api/${tableId}/rows/${rowId}`, url: `/api/${tableId}/rows/${rowId}`,
}) })
@ -16,6 +19,9 @@ export const fetchRow = async ({ tableId, rowId }) => {
* Creates a row in a table. * Creates a row in a table.
*/ */
export const saveRow = async row => { export const saveRow = async row => {
if (!row?.tableId) {
return
}
const res = await API.post({ const res = await API.post({
url: `/api/${row.tableId}/rows`, url: `/api/${row.tableId}/rows`,
body: row, body: row,
@ -23,6 +29,10 @@ export const saveRow = async row => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row saved") : notificationStore.success("Row saved")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId)
return res return res
} }
@ -30,6 +40,9 @@ export const saveRow = async row => {
* Updates a row in a table. * Updates a row in a table.
*/ */
export const updateRow = async row => { export const updateRow = async row => {
if (!row?.tableId || !row?._id) {
return
}
const res = await API.patch({ const res = await API.patch({
url: `/api/${row.tableId}/rows/${row._id}`, url: `/api/${row.tableId}/rows/${row._id}`,
body: row, body: row,
@ -37,6 +50,10 @@ export const updateRow = async row => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row updated") : notificationStore.success("Row updated")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(row.tableId)
return res return res
} }
@ -44,12 +61,19 @@ export const updateRow = async row => {
* Deletes a row from a table. * Deletes a row from a table.
*/ */
export const deleteRow = async ({ tableId, rowId, revId }) => { export const deleteRow = async ({ tableId, rowId, revId }) => {
if (!tableId || !rowId || !revId) {
return
}
const res = await API.del({ const res = await API.del({
url: `/api/${tableId}/rows/${rowId}/${revId}`, url: `/api/${tableId}/rows/${rowId}/${revId}`,
}) })
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success("Row deleted") : notificationStore.success("Row deleted")
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId)
return res return res
} }
@ -57,6 +81,9 @@ export const deleteRow = async ({ tableId, rowId, revId }) => {
* Deletes many rows from a table. * Deletes many rows from a table.
*/ */
export const deleteRows = async ({ tableId, rows }) => { export const deleteRows = async ({ tableId, rows }) => {
if (!tableId || !rows) {
return
}
const res = await API.post({ const res = await API.post({
url: `/api/${tableId}/rows`, url: `/api/${tableId}/rows`,
body: { body: {
@ -67,6 +94,10 @@ export const deleteRows = async ({ tableId, rows }) => {
res.error res.error
? notificationStore.danger("An error has occurred") ? notificationStore.danger("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`) : notificationStore.success(`${rows.length} row(s) deleted`)
// Refresh related datasources
datasourceStore.actions.invalidateDatasource(tableId)
return res return res
} }

View File

@ -1,6 +1,8 @@
<script> <script>
import { getContext, setContext } from "svelte" import { getContext, setContext, onMount } from "svelte"
import { createContextStore } from "../store" import { datasourceStore, createContextStore } from "../store"
import { ActionTypes } from "../constants"
import { generate } from "shortid"
export let data export let data
export let actions export let actions
@ -13,15 +15,40 @@
setContext("context", newContext) setContext("context", newContext)
$: providerKey = key || $component.id $: providerKey = key || $component.id
// Instance ID is unique to each instance of a provider
let instanceId
// Add data context // Add data context
$: data !== undefined && newContext.actions.provideData(providerKey, data) $: data !== undefined && newContext.actions.provideData(providerKey, data)
// Add actions context // Add actions context
$: { $: {
actions?.forEach(({ type, callback }) => { if (instanceId) {
newContext.actions.provideAction(providerKey, type, callback) actions?.forEach(({ type, callback, metadata }) => {
}) newContext.actions.provideAction(providerKey, type, callback)
// Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) {
const { datasource } = metadata || {}
datasourceStore.actions.registerDatasource(
datasource,
instanceId,
callback
)
}
})
}
} }
onMount(() => {
// Generate a permanent unique ID for this component and use it to register
// any datasource actions
instanceId = generate()
// Unregister all datasource instances when unmounting this provider
return () => datasourceStore.actions.unregisterInstance(instanceId)
})
</script> </script>
<slot /> <slot />

View File

@ -4,28 +4,28 @@ export const createContextStore = existingContext => {
const store = writable({ ...existingContext }) const store = writable({ ...existingContext })
// Adds a data context layer to the tree // Adds a data context layer to the tree
const provideData = (key, data) => { const provideData = (providerId, data) => {
if (!key) { if (!providerId) {
return return
} }
store.update(state => { store.update(state => {
state[key] = data state[providerId] = data
// Keep track of the closest component ID so we can later hydrate a "data" prop. // Keep track of the closest component ID so we can later hydrate a "data" prop.
// This is only required for legacy bindings that used "data" rather than a // This is only required for legacy bindings that used "data" rather than a
// component ID. // component ID.
state.closestComponentId = key state.closestComponentId = providerId
return state return state
}) })
} }
// Adds an action context layer to the tree // Adds an action context layer to the tree
const provideAction = (key, actionType, callback) => { const provideAction = (providerId, actionType, callback) => {
if (!key || !actionType) { if (!providerId || !actionType) {
return return
} }
store.update(state => { store.update(state => {
state[`${key}_${actionType}`] = callback state[`${providerId}_${actionType}`] = callback
return state return state
}) })
} }

View File

@ -0,0 +1,80 @@
import { writable, get } from "svelte/store"
export const createDatasourceStore = () => {
const store = writable([])
// Registers a new datasource instance
const registerDatasource = (datasource, instanceId, refresh) => {
if (!datasource || !instanceId || !refresh) {
return
}
// Create a list of all relevant datasource IDs which would require that
// this datasource is refreshed
let datasourceIds = []
// Extract table ID
if (datasource.type === "table") {
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract both table IDs from both sides of the relationship
else if (datasource.type === "link") {
if (datasource.rowTableId) {
datasourceIds.push(datasource.rowTableId)
}
if (datasource.tableId) {
datasourceIds.push(datasource.tableId)
}
}
// Extract the datasource ID (not the query ID) for queries
else if (datasource.type === "query") {
if (datasource.datasourceId) {
datasourceIds.push(datasource.datasourceId)
}
}
// Store configs for each relevant datasource ID
if (datasourceIds.length) {
store.update(state => {
datasourceIds.forEach(id => {
state.push({
datasourceId: id,
instanceId,
refresh,
})
})
return state
})
}
}
// Removes all registered datasource instances belonging to a particular
// instance ID
const unregisterInstance = instanceId => {
store.update(state => {
return state.filter(instance => instance.instanceId !== instanceId)
})
}
// Invalidates a specific datasource ID by refreshing all instances
// which depend on data from that datasource
const invalidateDatasource = datasourceId => {
const relatedInstances = get(store).filter(instance => {
return instance.datasourceId === datasourceId
})
relatedInstances?.forEach(instance => {
instance.refresh()
})
}
return {
subscribe: store.subscribe,
actions: { registerDatasource, unregisterInstance, invalidateDatasource },
}
}
export const datasourceStore = createDatasourceStore()

View File

@ -3,6 +3,7 @@ export { notificationStore } from "./notification"
export { routeStore } from "./routes" export { routeStore } from "./routes"
export { screenStore } from "./screens" export { screenStore } from "./screens"
export { builderStore } from "./builder" export { builderStore } from "./builder"
export { datasourceStore } from "./datasource"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -2,17 +2,23 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
export let datasource = []
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( const { API, styleable, Provider, builderStore, ActionTypes } = getContext(
"sdk" "sdk"
) )
const component = getContext("component") const component = getContext("component")
export let datasource = []
let rows = [] let rows = []
let loaded = false let loaded = false
$: fetchData(datasource) $: fetchData(datasource)
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(datasource),
metadata: { datasource },
},
]
async function fetchData(datasource) { async function fetchData(datasource) {
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
@ -20,13 +26,6 @@
} }
loaded = true loaded = true
} }
$: actions = [
{
type: ActionTypes.RefreshDatasource,
callback: () => fetchData(datasource),
},
]
</script> </script>
<Provider {actions}> <Provider {actions}>