diff --git a/hooks/useProtocolImport.tsx b/hooks/useProtocolImport.tsx index 18c85da2..352fb401 100644 --- a/hooks/useProtocolImport.tsx +++ b/hooks/useProtocolImport.tsx @@ -37,6 +37,8 @@ export const useProtocolImport = () => { const { mutateAsync: getProtocolExists } = api.protocol.get.byHash.useMutation(); + const { mutateAsync: getNewAssetIds } = api.asset.checkExisting.useMutation(); + /** * This is the main job processing function. Takes a file, and handles all * the steps required to import it into the database, updating the job @@ -160,9 +162,36 @@ export const useProtocolImport = () => { } const assets = await getProtocolAssets(protocolJson, zip); - let assetsWithCombinedMetadata: z.infer = []; - if (assets.length > 0) { + const newAssets: typeof assets = []; + + const existingAssetIds: string[] = []; + + let newAssetsWithCombinedMetadata: z.infer = []; + + // Check if the assets are already in the database. + // If yes, add them to existingAssetIds to be connected to the protocol. + // If not, add them to newAssets to be uploaded. + + try { + const newAssetIds = await getNewAssetIds( + assets.map((asset) => asset.assetId), + ); + + assets.forEach((asset) => { + if (newAssetIds.includes(asset.assetId)) { + newAssets.push(asset); + } else { + existingAssetIds.push(asset.assetId); + } + }); + } catch (e) { + throw new Error('Error checking for existing assets'); + } + + // Upload the new assets + + if (newAssets.length > 0) { dispatch({ type: 'UPDATE_STATUS', payload: { @@ -177,18 +206,18 @@ export const useProtocolImport = () => { * track the current bytes uploaded per file (uploads are done in * parallel). */ - const totalBytesToUpload = assets.reduce((acc, asset) => { + const totalBytesToUpload = newAssets.reduce((acc, asset) => { return acc + asset.file.size; }, 0); const currentBytesUploaded: Record = {}; - const files = assets.map((asset) => asset.file); + const files = newAssets.map((asset) => asset.file); const uploadedFiles = await uploadFiles('assetRouter', { files, onUploadProgress({ progress, file }) { - const thisFileSize = assets.find((asset) => asset.name === file)! + const thisFileSize = newAssets.find((asset) => asset.name === file)! .file.size; // eg. 1000 const thisCompletedBytes = thisFileSize * (progress / 100); @@ -222,11 +251,11 @@ export const useProtocolImport = () => { /** * We now need to merge the metadata from the uploaded files with the * asset metadata from the protocol json, so that we can insert the - * assets into the database. + * newassets into the database. * * The 'name' prop matches across both - we can use that to merge them. */ - assetsWithCombinedMetadata = assets.map((asset) => { + newAssetsWithCombinedMetadata = newAssets.map((asset) => { const uploadedAsset = uploadedFiles.find( (uploadedFile) => uploadedFile.name === asset.name, ); @@ -259,7 +288,8 @@ export const useProtocolImport = () => { const result = await insertProtocol({ protocol: protocolJson, protocolName: fileName, - assets: assetsWithCombinedMetadata, + newAssets: newAssetsWithCombinedMetadata, + existingAssetIds: existingAssetIds, }); if (result.error) { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 39b1d2cd..f3986392 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,10 +33,9 @@ model Asset { type String url String size Int - protocol Protocol @relation(fields: [protocolId], references: [id], onDelete: Cascade) - protocolId String // from db + protocols Protocol[] - @@index(fields: [protocolId, assetId, key]) + @@index(fields: [assetId, key]) } model Interview { diff --git a/server/router.ts b/server/router.ts index 71d3ba34..66cd0141 100644 --- a/server/router.ts +++ b/server/router.ts @@ -5,9 +5,11 @@ import { protocolRouter } from '~/server/routers/protocol'; import { participantRouter } from './routers/participant'; import { router } from './trpc'; import { dashboardRouter } from './routers/dashboard'; +import { assetRouter } from './routers/asset'; export const appRouter = router({ appSettings: appSettingsRouter, + asset: assetRouter, dashboard: dashboardRouter, session: sessionRouter, interview: interviewRouter, diff --git a/server/routers/asset.ts b/server/routers/asset.ts new file mode 100644 index 00000000..46e49321 --- /dev/null +++ b/server/routers/asset.ts @@ -0,0 +1,21 @@ +/* eslint-disable local-rules/require-data-mapper */ +import { prisma } from '~/utils/db'; +import { protectedProcedure, router } from '~/server/trpc'; +import { z } from 'zod'; + +export const assetRouter = router({ + checkExisting: protectedProcedure + .input(z.array(z.string())) + .mutation(async ({ input: assetIds }) => { + const assets = await prisma.asset.findMany({ + where: { + assetId: { + in: assetIds, + }, + }, + }); + const existingAssets = assets.map((asset) => asset.assetId); + // Return the assetIds that are not in the database + return assetIds.filter((assetId) => !existingAssets.includes(assetId)); + }), +}); diff --git a/server/routers/protocol.ts b/server/routers/protocol.ts index c8a5bd8e..a42180a5 100644 --- a/server/routers/protocol.ts +++ b/server/routers/protocol.ts @@ -27,8 +27,17 @@ export const deleteProtocols = async (hashes: string[]) => { select: { id: true, name: true }, }); + // Select assets that are ONLY associated with the protocols to be deleted const assets = await prisma.asset.findMany({ - where: { protocolId: { in: protocolsToBeDeleted.map((p) => p.id) } }, + where: { + protocols: { + every: { + id: { + in: protocolsToBeDeleted.map((p) => p.id), + }, + }, + }, + }, select: { key: true }, }); // We put asset deletion in a separate try/catch because if it fails, we still @@ -145,13 +154,19 @@ export const protocolRouter = router({ .object({ protocol: z.unknown(), // TODO: replace this with zod schema version of Protocol type protocolName: z.string(), - assets: assetInsertSchema, + newAssets: assetInsertSchema, + existingAssetIds: z.array(z.string()), }) .passthrough() .parse(value); }) .mutation(async ({ input }) => { - const { protocol: inputProtocol, protocolName, assets } = input; + const { + protocol: inputProtocol, + protocolName, + newAssets, + existingAssetIds, + } = input; const protocol = inputProtocol as Protocol; @@ -169,7 +184,8 @@ export const protocolRouter = router({ codebook: protocol.codebook, description: protocol.description, assets: { - create: assets, + create: newAssets, + connect: existingAssetIds.map((assetId) => ({ assetId })), }, }, }); @@ -189,8 +205,8 @@ export const protocolRouter = router({ return { error: null, success: true }; } catch (e) { // Attempt to delete any assets we uploaded to storage - if (assets.length > 0) { - await deleteFilesFromUploadThing(assets.map((a) => a.key)); + if (newAssets.length > 0) { + await deleteFilesFromUploadThing(newAssets.map((a) => a.key)); } // Check for protocol already existing if (e instanceof Prisma.PrismaClientKnownRequestError) {