Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: assetId not unique #93

Merged
merged 6 commits into from Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 38 additions & 8 deletions hooks/useProtocolImport.tsx
Expand Up @@ -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
Expand Down Expand Up @@ -160,9 +162,36 @@ export const useProtocolImport = () => {
}

const assets = await getProtocolAssets(protocolJson, zip);
let assetsWithCombinedMetadata: z.infer<typeof assetInsertSchema> = [];

if (assets.length > 0) {
const newAssets: typeof assets = [];

const existingAssetIds: string[] = [];

let newAssetsWithCombinedMetadata: z.infer<typeof assetInsertSchema> = [];

// 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: {
Expand All @@ -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<string, number> = {};

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);
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -259,7 +288,8 @@ export const useProtocolImport = () => {
const result = await insertProtocol({
protocol: protocolJson,
protocolName: fileName,
assets: assetsWithCombinedMetadata,
newAssets: newAssetsWithCombinedMetadata,
existingAssetIds: existingAssetIds,
});

if (result.error) {
Expand Down
5 changes: 2 additions & 3 deletions prisma/schema.prisma
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions server/router.ts
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions 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));
}),
});
28 changes: 22 additions & 6 deletions server/routers/protocol.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -169,7 +184,8 @@ export const protocolRouter = router({
codebook: protocol.codebook,
description: protocol.description,
assets: {
create: assets,
create: newAssets,
connect: existingAssetIds.map((assetId) => ({ assetId })),
},
},
});
Expand All @@ -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) {
Expand Down