Skip to content

Commit 2846ebe

Browse files
committed
Add new 'POST /vN/attachments/byIds' API endpoint
The new endpoint returns attachments by their IDs. Request body: {"ids": [...]}. Response body: { "attachments": [...], "users": [...], "idsNotFound": [...] }.
1 parent 76c511d commit 2846ebe

File tree

7 files changed

+87
-3
lines changed

7 files changed

+87
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5050
[app/serializers/v2/attachment.ts](app/serializers/v2/attachment.ts) file.
5151
- The new `GET /vN/attachments/:attId` API endpoint returns the attachment by
5252
its ID.
53+
- The new `POST /vN/attachments/byIds` API endpoint returns attachments by their
54+
IDs. Request body: `{"ids": [...]}`. Response body: `{ "attachments": [...],
55+
"users": [...], "idsNotFound": [...] }`.
5356
- The new `GET /vN/attachments/:attId/:type` API endpoint returns the preview or
5457
the original of the attachment. The _type_ parameter can be 'original',
5558
'image', 'video' or 'audio'. The returned data is a JSON object with the following

app/controllers/api/v1/AttachmentsController.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import compose from 'koa-compose';
33
import { isInt } from 'validator';
44
import { lookup } from 'mime-types';
55
import { mediaType } from '@hapi/accept';
6+
import { difference } from 'lodash';
67

78
import {
89
reportError,
@@ -12,11 +13,12 @@ import {
1213
} from '../../../support/exceptions';
1314
import { serializeAttachment } from '../../../serializers/v2/attachment';
1415
import { serializeUsersByIds } from '../../../serializers/v2/user';
15-
import { authRequired } from '../../middlewares';
16+
import { authRequired, inputSchemaRequired } from '../../middlewares';
1617
import { dbAdapter, Attachment } from '../../../models';
1718
import { startAttachmentsSanitizeJob } from '../../../jobs/attachments-sanitize';
1819
import { currentConfig } from '../../../support/app-async-context';
1920
import { getBestVariant } from '../../../support/media-files/geometry';
21+
import { getAttachmentsByIdsInputSchema } from '../v2/data-schemes/attachmants';
2022

2123
export default class AttachmentsController {
2224
app;
@@ -285,4 +287,36 @@ export default class AttachmentsController {
285287
ctx.body = response;
286288
}
287289
}
290+
291+
getByIds = compose([
292+
inputSchemaRequired(getAttachmentsByIdsInputSchema),
293+
async (ctx) => {
294+
const maxAttByIds = 100;
295+
296+
const { user: viewer, apiVersion } = ctx.state;
297+
const { ids } = ctx.request.body;
298+
299+
const hasMore = ids.length > maxAttByIds;
300+
301+
if (hasMore) {
302+
ids.length = maxAttByIds;
303+
}
304+
305+
const atts = (await dbAdapter.getAttachmentsByIds(ids)).filter(Boolean);
306+
307+
const attachments = atts.map((a) => serializeAttachment(a, apiVersion));
308+
const users = await serializeUsersByIds(
309+
atts.map((a) => a.userId),
310+
viewer?.id,
311+
);
312+
const idsFound = atts.map((p) => p.id);
313+
const idsNotFound = difference(ids, idsFound);
314+
315+
ctx.body = {
316+
attachments,
317+
users,
318+
idsNotFound,
319+
};
320+
},
321+
]);
288322
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import definitions from '../../v1/data-schemes/definitions';
2+
3+
export const getAttachmentsByIdsInputSchema = {
4+
$schema: 'http://json-schema.org/schema#',
5+
6+
definitions,
7+
8+
type: 'object',
9+
required: ['ids'],
10+
11+
properties: {
12+
ids: {
13+
type: 'array',
14+
items: { $ref: '#/definitions/uuid' },
15+
uniqueItems: true,
16+
default: [],
17+
},
18+
},
19+
};

app/models/auth-tokens/app-tokens-scopes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export const appTokensScopes = [
108108
'GET /vN/calendar/:username/:year/:month/:day',
109109
'GET /vN/attachments/:attId',
110110
'GET /vN/attachments/:attId/:type',
111+
'POST /vN/attachments/byIds',
111112
],
112113
},
113114
{

app/routes/api/v2/AttachmentsRoute.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ export default function addRoutes(app) {
88
app.post('/attachments/my/sanitize', controller.mySanitize);
99
app.get('/attachments/:attId', controller.getById);
1010
app.get('/attachments/:attId/:type', controller.getPreview);
11+
// We use POST here because this method can accept many post IDs
12+
app.post('/attachments/byIds', controller.getByIds);
1113
}

app/support/DbAdapter/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export class DbAdapter {
223223

224224
// Attachments
225225
getAttachmentById(id: UUID): Promise<Attachment | null>;
226+
getAttachmentsByIds(ids: UUID[]): Promise<(Attachment | null)[]>;
226227
getPostAttachments(id: UUID): Promise<UUID[]>;
227228
getAttachmentsOfPost(postId: UUID): Promise<Attachment[]>;
228229
listAttachments(options: ListAttachmentsOptions): Promise<Attachment[]>;

test/functional/attachments.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,12 @@ describe('Attachments', () => {
419419
});
420420

421421
describe('List attachments', () => {
422-
let mars;
422+
let mars, attIds;
423423
before(async () => {
424424
mars = await createTestUser('mars');
425425

426+
attIds = [];
427+
426428
for (let i = 0; i < 10; i++) {
427429
const data = new FormData();
428430
data.append(
@@ -431,7 +433,8 @@ describe('Attachments', () => {
431433
`test${i + 1}.txt`,
432434
);
433435
// eslint-disable-next-line no-await-in-loop
434-
await performJSONRequest('POST', '/v1/attachments', data, authHeaders(mars));
436+
const resp = await performJSONRequest('POST', '/v1/attachments', data, authHeaders(mars));
437+
attIds.push(resp.attachments.id);
435438
}
436439
});
437440

@@ -474,6 +477,27 @@ describe('Attachments', () => {
474477
}
475478
});
476479

480+
it(`should get Mars'es attachments by ids`, async () => {
481+
const otherId = '00000000-0000-4000-8000-000000000001';
482+
const ids = [...attIds.slice(0, 4), otherId];
483+
const resp = await performJSONRequest(
484+
'POST',
485+
'/v2/attachments/byIds',
486+
{ ids },
487+
authHeaders(mars),
488+
);
489+
expect(resp, 'to satisfy', {
490+
attachments: [
491+
{ fileName: 'test1.txt' },
492+
{ fileName: 'test2.txt' },
493+
{ fileName: 'test3.txt' },
494+
{ fileName: 'test4.txt' },
495+
],
496+
users: [{ id: mars.user.id }],
497+
idsNotFound: [otherId],
498+
});
499+
});
500+
477501
it(`should list the rest of Mars'es attachments`, async () => {
478502
const resp = await performJSONRequest(
479503
'GET',

0 commit comments

Comments
 (0)