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 4 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
55 changes: 48 additions & 7 deletions hooks/useProtocolImport.tsx
Expand Up @@ -37,6 +37,8 @@ export const useProtocolImport = () => {
const { mutateAsync: getProtocolExists } =
api.protocol.get.byHash.useMutation();

const { mutateAsync: getAsset } = api.asset.get.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,47 @@ export const useProtocolImport = () => {
}

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

const newAssets: typeof assets = [];

const assetsWithCombinedMetadata: z.infer<typeof assetInsertSchema> = [];

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

// Check if the assets are already in the database.
// If yes, add them to assetsWithCombinedMetadata to be connected to the protocol.
// If not, add them to newAssets to be uploaded.

if (assets.length > 0) {
const assetPromises = assets.map(async (asset) => {
buckhalt marked this conversation as resolved.
Show resolved Hide resolved
const existingAsset = await getAsset(asset.assetId);
return existingAsset
? {
key: existingAsset.key,
assetId: asset.assetId,
name: asset.name,
type: asset.type,
url: existingAsset.url,
size: existingAsset.size,
}
: asset;
});

const resolvedAssets = await Promise.all(assetPromises);

// If the asset has a key, it's an existing asset
resolvedAssets.forEach((resolvedAsset) => {
if ('key' in resolvedAsset) {
assetsWithCombinedMetadata.push(resolvedAsset);
} else {
newAssets.push(resolvedAsset);
}
});
}

// we're going to upload the new assets

if (newAssets.length > 0) {
dispatch({
type: 'UPDATE_STATUS',
payload: {
Expand All @@ -177,18 +217,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 +262,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 +299,8 @@ export const useProtocolImport = () => {
const result = await insertProtocol({
protocol: protocolJson,
protocolName: fileName,
assets: assetsWithCombinedMetadata,
newAssets: newAssetsWithCombinedMetadata,
existingAssets: assetsWithCombinedMetadata,
});

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
17 changes: 17 additions & 0 deletions server/routers/asset.ts
@@ -0,0 +1,17 @@
/* 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({
get: protectedProcedure
.input(z.string())
.mutation(async ({ input: assetId }) => {
const asset = await prisma.asset.findFirst({
where: {
assetId,
},
});
return asset;
}),
});
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,
existingAssets: assetInsertSchema,
})
.passthrough()
.parse(value);
})
.mutation(async ({ input }) => {
const { protocol: inputProtocol, protocolName, assets } = input;
const {
protocol: inputProtocol,
protocolName,
newAssets,
existingAssets,
} = 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: existingAssets.map((a) => ({ key: a.key })),
},
},
});
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