Automatically refresh data when related data changes
This commit is contained in:
parent
7c3ccf69f9
commit
fe00c66700
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -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"
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
Loading…
Reference in New Issue