Skip to content

Commit 9c48b77

Browse files
authored
fix(files-upload): return simple error if exists (#1866)
* fix(files-upload): return simple error if exists * refactor: remove unused import
1 parent 09fa558 commit 9c48b77

5 files changed

Lines changed: 52 additions & 78 deletions

File tree

src/services/item/item.controller.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
getAccessible,
2020
getChildren,
2121
getDescendantItems,
22-
getMany,
2322
getOne,
2423
getParentItems,
2524
} from './item.schemas.packed';

src/services/item/item.schemas.packed.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,6 @@ export const getOne = {
5151
response: { [StatusCodes.OK]: packedItemSchemaRef, '4xx': errorSchemaRef },
5252
} as const satisfies FastifySchema;
5353

54-
export const getMany = {
55-
operationId: 'getManyItems',
56-
tags: ['item'],
57-
summary: 'Get many items',
58-
description: 'Get many items by their ids.',
59-
60-
querystring: customType.StrictObject({
61-
id: Type.Array(customType.UUID(), {
62-
maxItems: MAX_TARGETS_FOR_READ_REQUEST,
63-
uniqueItems: true,
64-
}),
65-
}),
66-
response: {
67-
[StatusCodes.OK]: Type.Object({
68-
data: Type.Record(Type.String({ format: 'uuid' }), packedItemSchemaRef),
69-
errors: Type.Array(errorSchemaRef),
70-
}),
71-
'4xx': errorSchemaRef,
72-
},
73-
} as const satisfies FastifySchema;
74-
7554
export const getAccessible = {
7655
operationId: 'getAccessibleItems',
7756
tags: ['item'],

src/services/item/plugins/file/itemFile.controller.test.ts

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NotFound } from '@aws-sdk/client-s3';
22
import assert from 'assert';
3-
import { eq, inArray } from 'drizzle-orm';
3+
import { and, eq, inArray } from 'drizzle-orm';
44
import FormData from 'form-data';
55
import fs from 'fs';
66
import { ReasonPhrases, StatusCodes } from 'http-status-codes';
@@ -25,8 +25,8 @@ import build, {
2525
import { MULTIPLE_ITEMS_LOADING_TIME } from '../../../../../test/constants';
2626
import { buildFile, seedFromJson } from '../../../../../test/mocks/seed';
2727
import { db } from '../../../../drizzle/db';
28+
import { isDirectChild } from '../../../../drizzle/operations';
2829
import { itemMembershipsTable, itemsRawTable } from '../../../../drizzle/schema';
29-
import { type ItemRaw } from '../../../../drizzle/types';
3030
import { assertIsDefined } from '../../../../utils/assertions';
3131
import { ITEMS_ROUTE_PREFIX } from '../../../../utils/config';
3232
import { MemberCannotAccess, MemberCannotWriteItem } from '../../../../utils/errors';
@@ -38,7 +38,7 @@ import {
3838
UploadFileUnexpectedError,
3939
} from '../../../file/utils/errors';
4040
import { ThumbnailSizeFormat } from '../../../thumbnail/constants';
41-
import { expectItem, expectManyItems } from '../../test/fixtures/items';
41+
import { expectItem } from '../../test/fixtures/items';
4242
import { DEFAULT_MAX_STORAGE } from './utils/constants';
4343
import { StorageExceeded } from './utils/errors';
4444

@@ -91,6 +91,18 @@ const createFormData = (form = new FormData(), filepath: string = './test/fixtur
9191
return form;
9292
};
9393

94+
const getItemsForActorId = async (actorId: string, parentPath?: string) => {
95+
const conditions = [eq(itemsRawTable.creatorId, actorId)];
96+
97+
if (parentPath) {
98+
conditions.push(isDirectChild(itemsRawTable.path, parentPath));
99+
}
100+
101+
return await db.query.itemsRawTable.findMany({
102+
where: and(...conditions),
103+
});
104+
};
105+
94106
describe('File Item routes tests', () => {
95107
let app: FastifyInstance;
96108

@@ -137,12 +149,10 @@ describe('File Item routes tests', () => {
137149
headers: form.getHeaders(),
138150
});
139151
// check response value
140-
const [newItem] = Object.values(response.json().data) as ItemRaw[];
141-
expect(response.statusCode).toBe(StatusCodes.OK);
152+
expect(response.statusCode).toBe(StatusCodes.NO_CONTENT);
142153

143154
// check item exists in db
144-
const item = await getItemById(newItem.id);
145-
expect(item).toBeDefined();
155+
const [item] = await getItemsForActorId(actor.id);
146156

147157
// s3 upload function: We expect on image AND the thumbnails
148158
expect(uploadDoneMock).toHaveBeenCalledTimes(
@@ -154,7 +164,7 @@ describe('File Item routes tests', () => {
154164
expect(item?.extra[ItemType.FILE]).toBeTruthy();
155165

156166
// a membership is created for this item
157-
const membership = await getItemMembershipByPath(newItem.path);
167+
const membership = await getItemMembershipByPath(item.path);
158168
expect(membership?.permission).toEqual(PermissionLevel.Admin);
159169
});
160170

@@ -172,12 +182,10 @@ describe('File Item routes tests', () => {
172182
headers: form.getHeaders(),
173183
});
174184
// check response value
175-
const [newItem] = Object.values(response.json().data) as ItemRaw[];
176-
expect(response.statusCode).toBe(StatusCodes.OK);
185+
expect(response.statusCode).toBe(StatusCodes.NO_CONTENT);
177186

178187
// check item exists in db
179-
const item = await getItemById(newItem.id);
180-
expectItem(item, newItem);
188+
const [item] = await getItemsForActorId(actor.id);
181189

182190
// s3 upload function: We expect on pdf and the thumbnails
183191
expect(uploadDoneMock).toHaveBeenCalledTimes(
@@ -202,17 +210,11 @@ describe('File Item routes tests', () => {
202210
});
203211

204212
// check response value
205-
const items = Object.values(response.json().data) as ItemRaw[];
206-
expect(response.statusCode).toBe(StatusCodes.OK);
213+
expect(response.statusCode).toBe(StatusCodes.NO_CONTENT);
207214

208-
// check item exists in db
209-
const newItems = await db.query.itemsRawTable.findMany({
210-
where: inArray(
211-
itemsRawTable.id,
212-
items.map(({ id }) => id),
213-
),
214-
});
215-
expectManyItems(items, newItems);
215+
// check items exist in db
216+
const newItems = await getItemsForActorId(actor.id);
217+
expect(newItems).toHaveLength(2);
216218

217219
// s3 upload function: We expect on image AND the thumbnails
218220
expect(uploadDoneMock).toHaveBeenCalledTimes(
@@ -228,7 +230,7 @@ describe('File Item routes tests', () => {
228230
const memberships = await db.query.itemMembershipsTable.findMany({
229231
where: inArray(
230232
itemMembershipsTable.itemPath,
231-
items.map(({ path }) => path),
233+
newItems.map(({ path }) => path),
232234
),
233235
});
234236
for (const m of memberships) {
@@ -255,12 +257,10 @@ describe('File Item routes tests', () => {
255257
});
256258

257259
// check response value
258-
const [newItem] = Object.values(response.json().data) as ItemRaw[];
259-
expect(response.statusCode).toBe(StatusCodes.OK);
260+
expect(response.statusCode).toBe(StatusCodes.NO_CONTENT);
260261

261262
// check item exists in db
262-
const item = await getItemById(newItem.id);
263-
expectItem(item, newItem);
263+
const [newItem] = await getItemsForActorId(actor.id, parentItem.path);
264264

265265
// s3 upload function: We expect on image AND the thumbnails
266266
expect(uploadDoneMock).toHaveBeenCalledTimes(
@@ -269,8 +269,8 @@ describe('File Item routes tests', () => {
269269

270270
// check file properties
271271
// TODO: more precise check
272-
expect(item?.extra[ItemType.FILE]).toBeTruthy();
273-
expect(item?.path).toContain(parentItem.path);
272+
expect(newItem?.extra[ItemType.FILE]).toBeTruthy();
273+
expect(newItem?.path).toContain(parentItem.path);
274274

275275
// a membership is not created for new item because it inherits parent
276276
const membership = await db.query.itemMembershipsTable.findFirst({
@@ -298,9 +298,9 @@ describe('File Item routes tests', () => {
298298
});
299299

300300
// check the response value
301-
expect(response.statusCode).toBe(StatusCodes.OK);
302-
const newItems = Object.values(response.json().data) as ItemRaw[];
303-
expect(newItems.length).toBe(2);
301+
expect(response.statusCode).toBe(StatusCodes.NO_CONTENT);
302+
const newItems = await getItemsForActorId(actor.id);
303+
expect(newItems).toHaveLength(2);
304304

305305
// check that both items exist in db and that their types are correctly interpreted
306306
const imageItem = await getItemById(newItems[0].id);
@@ -352,7 +352,7 @@ describe('File Item routes tests', () => {
352352
headers: form.getHeaders(),
353353
});
354354

355-
expect(response.json().errors[0]).toMatchObject(new StorageExceeded(expect.anything()));
355+
expect(response.json()).toMatchObject(new StorageExceeded(expect.anything()));
356356
});
357357

358358
it('Cannot upload empty file', async () => {
@@ -374,7 +374,7 @@ describe('File Item routes tests', () => {
374374
headers: form.getHeaders(),
375375
});
376376

377-
expect(response.json().errors[0].message).toEqual(new UploadEmptyFileError().message);
377+
expect(response.json().message).toEqual(new UploadEmptyFileError().message);
378378
expect(deleteObjectsMock).toHaveBeenCalled();
379379
});
380380
});
@@ -404,26 +404,24 @@ describe('File Item routes tests', () => {
404404
headers: form1.getHeaders(),
405405
});
406406

407-
expect(response.statusCode).toEqual(StatusCodes.OK);
407+
expect(response.statusCode).not.toBe(StatusCodes.NO_CONTENT);
408+
409+
// one empty file error
410+
expect(response.json().message).toEqual(new UploadEmptyFileError().message);
411+
408412
// upload 2 files and one set of thumbnails
409413
expect(uploadDoneMock).toHaveBeenCalledTimes(
410414
Object.values(ThumbnailSizeFormat).length + 2,
411415
);
412416
expect(deleteObjectsMock).toHaveBeenCalledTimes(1);
413417

414-
// one empty file error
415-
expect(response.json().errors[0].message).toEqual(new UploadEmptyFileError().message);
416-
417418
// one file has been uploaded
418-
const uploadedItems = Object.values<ItemRaw>(response.json().data);
419+
const uploadedItems = await getItemsForActorId(actor.id);
419420
expect(uploadedItems).toHaveLength(1);
420421

421-
// check item exists in db
422-
const item = await getItemById(uploadedItems[0].id);
423-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
424-
expect(item!.type).toEqual(ItemType.FILE);
422+
expect(uploadedItems[0].type).toEqual(ItemType.FILE);
425423
});
426-
it('Gracefully fails if s3 upload throws', async () => {
424+
it('Throws if s3 upload throws', async () => {
427425
uploadDoneMock.mockImplementation(() => {
428426
throw new Error('putObject throws');
429427
});
@@ -440,9 +438,7 @@ describe('File Item routes tests', () => {
440438
headers: form.getHeaders(),
441439
});
442440

443-
expect(response.json().errors[0]).toMatchObject(
444-
new UploadFileUnexpectedError(expect.anything()),
445-
);
441+
expect(response.json()).toMatchObject(new UploadFileUnexpectedError(expect.anything()));
446442
});
447443
});
448444
});

src/services/item/plugins/file/itemFile.controller.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const basePlugin: FastifyPluginAsyncTypebox<GraaspPluginFileOptions> = async (fa
106106
fastify.post('/upload', {
107107
schema: upload,
108108
preHandler: [isAuthenticated, matchOne(validatedMemberAccountRole)],
109-
handler: async (request) => {
109+
handler: async (request, reply) => {
110110
const {
111111
user,
112112
query: { id: parentId, previousItemId },
@@ -165,6 +165,7 @@ const basePlugin: FastifyPluginAsyncTypebox<GraaspPluginFileOptions> = async (fa
165165
} finally {
166166
// force close to avoid hanging
167167
// necessary for errors that don't read the stream
168+
log.debug('close file stream');
168169
stream.emit('end');
169170
}
170171
});
@@ -175,10 +176,12 @@ const basePlugin: FastifyPluginAsyncTypebox<GraaspPluginFileOptions> = async (fa
175176
await itemService.rescaleOrderForParent(db, member, items[0]);
176177
}
177178

178-
return {
179-
data: items.reduce((data, item) => ({ ...data, [item.id]: item }), {}),
180-
errors,
181-
};
179+
// return first error only
180+
if (errors.length) {
181+
throw errors[0];
182+
}
183+
184+
reply.status(StatusCodes.NO_CONTENT).send();
182185
},
183186
});
184187

src/services/item/plugins/file/itemFile.schema.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ export const upload = {
2222
),
2323
}),
2424
response: {
25-
[StatusCodes.OK]: customType.StrictObject({
26-
data: Type.Record(customType.UUID(), itemSchemaRef),
27-
errors: Type.Array(errorSchemaRef),
28-
}),
25+
[StatusCodes.NO_CONTENT]: Type.Null({ description: 'Successful response' }),
2926
'4xx': errorSchemaRef,
3027
},
3128
};

0 commit comments

Comments
 (0)