From befa006359dc6dab90ef8e8a5da3e8363286e1a6 Mon Sep 17 00:00:00 2001 From: Jamie Gaehring Date: Thu, 23 Feb 2023 22:41:15 -0500 Subject: [PATCH] Sync and cache file data at checkout. --- packages/field-kit/src/entities/index.js | 57 ++++++++++++++++--- packages/field-kit/src/idb/files.js | 16 +++++- .../src/ObservationsContainer.vue | 13 +++-- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/packages/field-kit/src/entities/index.js b/packages/field-kit/src/entities/index.js index b9d4b47a..00e74925 100644 --- a/packages/field-kit/src/entities/index.js +++ b/packages/field-kit/src/entities/index.js @@ -3,8 +3,8 @@ import { } from 'vue'; import { useObjectUrl } from '@vueuse/core'; import { - assoc, clone, complement, compose, curryN, equals, is, - map, mapObjIndexed, pick, propEq, when, + assoc, clone, complement, compose, curryN, equals, filter as rFilter, + is, map, mapObjIndexed, pick, propEq, when, } from 'ramda'; import { validate, v4 as uuidv4 } from 'uuid'; import { splitFilterByType } from 'farmos'; @@ -16,12 +16,13 @@ import { syncEntities } from '../http/sync'; import SyncScheduler from '../http/SyncScheduler'; import { getRecords } from '../idb'; import { cacheEntity } from '../idb/cache'; -import { cacheFileData, fmtFileData } from '../idb/files'; +import { cacheFileData, fmtFileData, loadFileEntity } from '../idb/files'; import { isArrayLike } from '../utils/asArray'; import diff from '../utils/diff'; import parseFilter from '../utils/parseFilter'; import { PromiseQueue } from '../utils/promises'; import flattenEntity from '../utils/flattenEntity'; +import upsert from '../utils/upsert'; import { alert } from '../warnings/alert'; import nomenclature from './nomenclature'; import { @@ -421,7 +422,7 @@ export default function useEntities(options = {}) { function useFile(fileData) { const file = clone(fileData); - if (!file.url && file.data instanceof Blob) file.url = useObjectUrl(file.data); + if (file.data instanceof Blob) file.url = useObjectUrl(file.data); return reactive(file); } @@ -429,10 +430,45 @@ export default function useEntities(options = {}) { const revision = revisions.get(reference); if (!is(Object, revision.files)) revision.files = {}; if (!Array.isArray(revision.files[field]) || !isProxy(revision.files[field])) { - revision.files[field] = shallowReactive([]); + revision.files[field] = reactive([]); } - const { files: { [field]: files } } = revision; - return readonly(files); + + // A safe helper for getting file entity identifiers from the host entity. + const getResources = state => state?.relationships?.[field] || []; + revision.queue.push(refState => Promise.all(getResources(refState).map(async (resource) => { + const init = await loadFileEntity(resource); + const fileData = init === null ? fmtFileData(null, null, resource) : init; + const file = useFile(fileData); + upsert(revision.files[field], 'id', file); + revision.files[field].push(file); + if (init === null) return cacheFileData(null, null, resource); + return fileData; + })).then(async (fileData) => { + if (validate(fileData.file_entity?.id)) return fileData; + // Normally the fileData's id and type can't be assumed to be the same as + // the file_entity, but if there's no file_entity, then we can be sure the + // resource identifier was provided as the fileData's id and type above. + const { id, type } = fileData; + const { data: [file_entity] } = await farm.file.fetch({ filter: { id, type } }); + if (!file_entity?.attributes?.uri?.url) { + return cacheFileData(null, file_entity, { id, type }); + } + const { attributes: { uri: { url }, filemime } } = file_entity; + const headers = { + Accept: filemime || 'application/octet-stream', + // 'Accept-Encoding': '*', + // Connection: 'close', + }; + const { data } = await farm.remote.request({ + headers, responseType: 'blob', url, + }); + const updated = fmtFileData(data, file_entity, { id, type }); + const file = useFile(updated); + upsert(revision.files[field], 'id', file); + return cacheFileData(data, file_entity, { id, type }); + }).then(() => refState).catch(() => refState)); + + return readonly(revision.files[field]); } function attachFile(reference, field, file) { @@ -549,8 +585,11 @@ export default function useEntities(options = {}) { }) .then((cache) => { const syncOptions = { cache, filter: { id, type } }; - const { length: fileCount } = Object.values(revision.files || {}).flat(); - if (fileCount > 0) syncOptions.files = { [id]: revision.files }; + // File data should only be synced once. + const takeUnsyncedFiles = rFilter(fileData => fileData.file_entity); + const files = map(takeUnsyncedFiles, revision.files); + const { length: fileCount } = Object.values(files || {}).flat(); + if (fileCount > 0) syncOptions.files = { [id]: files }; return syncEntities(shortName, syncOptions) .then(syncHandler(revision)) .then(({ data: [final] = [] } = {}) => final || cache); diff --git a/packages/field-kit/src/idb/files.js b/packages/field-kit/src/idb/files.js index 210fa06a..1462a8c0 100644 --- a/packages/field-kit/src/idb/files.js +++ b/packages/field-kit/src/idb/files.js @@ -1,5 +1,5 @@ import { validate, v4 as uuidv4 } from 'uuid'; -import { getRecords, saveRecord } from '.'; +import { getRecords, getRecordsFromIndex, saveRecord } from '.'; import dbs from './databases.js'; const validStores = dbs.binary_data.stores.map(s => s.name); @@ -33,7 +33,7 @@ export function fmtFileData(data = null, file_entity = null, options = {}) { const { mime = data?.type || filemime || defaultMime, references = [], - url = uri.value || null, + url = uri.url || null, } = options; let { filename = data?.name || entity_filename, id = entity_id, type } = options; if (!validate(id)) id = uuidv4(); @@ -91,3 +91,15 @@ export async function loadFilesByHostId(fileIdentifiers) { const resultsByHostId = await Promise.all(requestsByHostId); return Object.fromEntries(resultsByHostId); } + +export function loadFileEntity(fileEntity) { + const db = 'binary_data'; + const index = 'file_entity_id'; + function loader(entity, stores) { + if (stores.length <= 0) return null; + const [store, ...tail] = stores; + return getRecordsFromIndex(db, store, index, entity.id) + .catch(() => loader(entity, tail)); + } + return loader(fileEntity, validStores); +} diff --git a/packages/field-module-observations/src/ObservationsContainer.vue b/packages/field-module-observations/src/ObservationsContainer.vue index de001c6c..17fe6707 100644 --- a/packages/field-module-observations/src/ObservationsContainer.vue +++ b/packages/field-module-observations/src/ObservationsContainer.vue @@ -31,12 +31,13 @@ -
+
@@ -105,8 +106,10 @@ export default { attachFile(current, 'image', filelist); } - const images = computed(() => - (current.value ? restoreFiles(current.value, 'image') : [])); + const images = computed(() => { + if (!current.value) return []; + return restoreFiles(current.value, 'image'); + }); const save = () => commit(current.value); const cancel = () => { currentID.value = undefined; };