Skip to content

Commit 6d73b60

Browse files
authored
feat: add document endpoints (#1711)
* feat: add document endpoints * refactor: remove typo * refactor: apply refactor
1 parent cd50e7f commit 6d73b60

6 files changed

Lines changed: 775 additions & 0 deletions

File tree

src/services/item/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import graaspItemLogin from '../itemLogin';
1515
import itemController from './controller';
1616
import actionItemPlugin from './plugins/action';
1717
import graaspApps from './plugins/app';
18+
import graaspDocumentItem from './plugins/document/controller';
19+
import { PREFIX_DOCUMENT } from './plugins/document/service';
1820
import graaspEmbeddedLinkItem from './plugins/embeddedLink/controller';
1921
import { PREFIX_EMBEDDED_LINK } from './plugins/embeddedLink/service';
2022
import graaspEnrollPlugin from './plugins/enroll';
@@ -96,6 +98,8 @@ const plugin: FastifyPluginAsync = async (fastify) => {
9698
prefix: PREFIX_EMBEDDED_LINK,
9799
});
98100

101+
fastify.register(graaspDocumentItem, { prefix: PREFIX_DOCUMENT });
102+
99103
fastify.register(graaspInvitationsPlugin);
100104

101105
fastify.register(graaspEnrollPlugin);
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import { StatusCodes } from 'http-status-codes';
2+
3+
import { FastifyInstance } from 'fastify';
4+
5+
import { DocumentItemExtraFlavor, HttpMethod, ItemType, MemberFactory } from '@graasp/sdk';
6+
7+
import build, {
8+
clearDatabase,
9+
mockAuthenticate,
10+
unmockAuthenticate,
11+
} from '../../../../../test/app';
12+
import { AppDataSource } from '../../../../plugins/datasource';
13+
import { Guest } from '../../../itemLogin/entities/guest';
14+
import { Member } from '../../../member/entities/member';
15+
import { saveMember } from '../../../member/test/fixtures/members';
16+
import { ItemTestUtils } from '../../test/fixtures/items';
17+
18+
const testUtils = new ItemTestUtils();
19+
const rawGuestRepository = AppDataSource.getRepository(Guest);
20+
21+
describe('Document Item tests', () => {
22+
let app: FastifyInstance;
23+
let actor: Member | undefined;
24+
25+
beforeAll(async () => {
26+
({ app } = await build({ member: null }));
27+
});
28+
29+
afterAll(async () => {
30+
await clearDatabase(app.db);
31+
app.close();
32+
});
33+
34+
afterEach(async () => {
35+
jest.clearAllMocks();
36+
actor = undefined;
37+
unmockAuthenticate();
38+
});
39+
40+
describe('POST /items/documents', () => {
41+
it('Throws if signed out', async () => {
42+
const payload = { name: 'name', content: 'content' };
43+
44+
const response = await app.inject({
45+
method: HttpMethod.Post,
46+
url: '/items/documents',
47+
payload,
48+
});
49+
50+
expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED);
51+
});
52+
it('Throws if actor is guest', async () => {
53+
const payload = { name: 'name', content: 'content' };
54+
const actor = await rawGuestRepository.save({ name: 'guest' });
55+
mockAuthenticate(actor);
56+
const response = await app.inject({
57+
method: HttpMethod.Post,
58+
url: '/items/documents',
59+
payload,
60+
});
61+
62+
expect(response.statusCode).toBe(StatusCodes.FORBIDDEN);
63+
});
64+
it('Throws if actor is not validated', async () => {
65+
const payload = { name: 'name', content: 'content' };
66+
const actor = await saveMember(MemberFactory({ isValidated: false }));
67+
mockAuthenticate(actor);
68+
const response = await app.inject({
69+
method: HttpMethod.Post,
70+
url: '/items/documents',
71+
payload,
72+
});
73+
74+
expect(response.statusCode).toBe(StatusCodes.FORBIDDEN);
75+
});
76+
it('Throws if content is empty', async () => {
77+
const actor = await saveMember(MemberFactory());
78+
mockAuthenticate(actor);
79+
const payload = { name: 'name', content: '' };
80+
81+
const response = await app.inject({
82+
method: HttpMethod.Post,
83+
url: '/items/documents',
84+
payload,
85+
});
86+
87+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
88+
});
89+
it('Throws if isRaw is invalid', async () => {
90+
const actor = await saveMember(MemberFactory());
91+
mockAuthenticate(actor);
92+
const payload = { name: 'name', content: 'content', isRaw: 'value' };
93+
94+
const response = await app.inject({
95+
method: HttpMethod.Post,
96+
url: '/items/documents',
97+
payload,
98+
});
99+
100+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
101+
});
102+
it('Throws if flavor is invalid', async () => {
103+
const actor = await saveMember(MemberFactory());
104+
mockAuthenticate(actor);
105+
const payload = { name: 'name', content: 'content', flavor: 'value' };
106+
107+
const response = await app.inject({
108+
method: HttpMethod.Post,
109+
url: '/items/documents',
110+
payload,
111+
});
112+
113+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
114+
});
115+
116+
describe('Signed In', () => {
117+
beforeEach(async () => {
118+
actor = await saveMember();
119+
mockAuthenticate(actor);
120+
});
121+
122+
it('Create successfully', async () => {
123+
const response = await app.inject({
124+
method: HttpMethod.Post,
125+
url: '/items/documents',
126+
payload: { name: 'name', content: 'content' },
127+
});
128+
129+
expect(response.statusCode).toBe(StatusCodes.OK);
130+
});
131+
});
132+
});
133+
134+
describe('PATCH /items/documents/:id', () => {
135+
it('Throws if signed out', async () => {
136+
const member = await saveMember();
137+
const { item } = await testUtils.saveItemAndMembership({ member });
138+
139+
const response = await app.inject({
140+
method: HttpMethod.Patch,
141+
url: `/items/documents/${item.id}`,
142+
payload: { name: 'new name' },
143+
});
144+
145+
expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED);
146+
});
147+
it('Throws if actor is guest', async () => {
148+
const actor = await rawGuestRepository.save({ name: 'guest' });
149+
const { item } = await testUtils.saveItemAndMembership({ member: actor });
150+
mockAuthenticate(actor);
151+
const response = await app.inject({
152+
method: HttpMethod.Patch,
153+
url: `/items/documents/${item.id}`,
154+
payload: { name: 'new name' },
155+
});
156+
157+
expect(response.statusCode).toBe(StatusCodes.FORBIDDEN);
158+
});
159+
it('Throws if actor is not validated', async () => {
160+
const actor = await saveMember(MemberFactory({ isValidated: false }));
161+
const { item } = await testUtils.saveItemAndMembership({ member: actor });
162+
mockAuthenticate(actor);
163+
const response = await app.inject({
164+
method: HttpMethod.Patch,
165+
url: `/items/documents/${item.id}`,
166+
payload: { name: 'new name' },
167+
});
168+
169+
expect(response.statusCode).toBe(StatusCodes.FORBIDDEN);
170+
});
171+
it('Throws if content is empty', async () => {
172+
const actor = await saveMember(MemberFactory());
173+
mockAuthenticate(actor);
174+
const payload = { name: 'name', content: '' };
175+
const { item } = await testUtils.saveItemAndMembership({ member: actor });
176+
177+
const response = await app.inject({
178+
method: HttpMethod.Patch,
179+
url: `/items/documents/${item.id}`,
180+
payload,
181+
});
182+
183+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
184+
});
185+
it('Throws if isRaw is invalid', async () => {
186+
const actor = await saveMember(MemberFactory());
187+
mockAuthenticate(actor);
188+
const { item } = await testUtils.saveItemAndMembership({ member: actor });
189+
const payload = { name: 'name', content: 'content', isRaw: 'value' };
190+
191+
const response = await app.inject({
192+
method: HttpMethod.Patch,
193+
url: `/items/documents/${item.id}`,
194+
payload,
195+
});
196+
197+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
198+
});
199+
it('Throws if flavor is invalid', async () => {
200+
const actor = await saveMember(MemberFactory());
201+
mockAuthenticate(actor);
202+
const { item } = await testUtils.saveItemAndMembership({ member: actor });
203+
const payload = { name: 'name', content: 'content', flavor: 'value' };
204+
205+
const response = await app.inject({
206+
method: HttpMethod.Patch,
207+
url: `/items/documents/${item.id}`,
208+
payload,
209+
});
210+
211+
expect(response.statusCode).toBe(StatusCodes.BAD_REQUEST);
212+
});
213+
214+
describe('Signed In', () => {
215+
beforeEach(async () => {
216+
actor = await saveMember();
217+
mockAuthenticate(actor);
218+
});
219+
220+
it('Update successfully', async () => {
221+
const { item } = await testUtils.saveItemAndMembership({
222+
item: {
223+
type: ItemType.DOCUMENT,
224+
extra: {
225+
[ItemType.DOCUMENT]: {
226+
content: 'value',
227+
},
228+
},
229+
},
230+
member: actor,
231+
});
232+
const payload = {
233+
name: 'new name',
234+
content: 'new value',
235+
// test that flavor can be updated
236+
flavor: DocumentItemExtraFlavor.Info,
237+
238+
settings: {
239+
hasThumbnail: true,
240+
isCollapsible: true,
241+
},
242+
};
243+
244+
const response = await app.inject({
245+
method: HttpMethod.Patch,
246+
url: `/items/documents/${item.id}`,
247+
payload,
248+
});
249+
250+
expect(response.statusCode).toBe(StatusCodes.OK);
251+
});
252+
});
253+
});
254+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
2+
3+
import { resolveDependency } from '../../../../di/utils';
4+
import { asDefined } from '../../../../utils/assertions';
5+
import { buildRepositories } from '../../../../utils/repositories';
6+
import { isAuthenticated } from '../../../auth/plugins/passport';
7+
import { matchOne } from '../../../authorization';
8+
import { assertIsMember } from '../../../member/entities/member';
9+
import { validatedMemberAccountRole } from '../../../member/strategies/validatedMemberAccountRole';
10+
import { ActionItemService } from '../action/service';
11+
import { createDocument, updateDocument } from './schemas';
12+
import { DocumentItemService } from './service';
13+
14+
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
15+
const { db } = fastify;
16+
const documentService = resolveDependency(DocumentItemService);
17+
const actionItemService = resolveDependency(ActionItemService);
18+
19+
fastify.post(
20+
'/',
21+
{
22+
schema: createDocument,
23+
preHandler: [isAuthenticated, matchOne(validatedMemberAccountRole)],
24+
},
25+
async (request, reply) => {
26+
const {
27+
user,
28+
query: { parentId, previousItemId },
29+
body: data,
30+
} = request;
31+
const member = asDefined(user?.account);
32+
assertIsMember(member);
33+
34+
const item = await db.transaction(async (manager) => {
35+
const repositories = buildRepositories(manager);
36+
const item = await documentService.postWithOptions(member, repositories, {
37+
...data,
38+
previousItemId,
39+
parentId,
40+
});
41+
return item;
42+
});
43+
44+
reply.send(item);
45+
46+
// background operations
47+
await actionItemService.postPostAction(request, buildRepositories(), item);
48+
await db.transaction(async (manager) => {
49+
const repositories = buildRepositories(manager);
50+
await documentService.rescaleOrderForParent(member, repositories, item);
51+
});
52+
},
53+
);
54+
55+
fastify.patch(
56+
'/:id',
57+
{
58+
schema: updateDocument,
59+
preHandler: [isAuthenticated, matchOne(validatedMemberAccountRole)],
60+
},
61+
async (request) => {
62+
const {
63+
user,
64+
params: { id },
65+
body,
66+
} = request;
67+
const member = asDefined(user?.account);
68+
assertIsMember(member);
69+
return await db.transaction(async (manager) => {
70+
const repositories = buildRepositories(manager);
71+
const item = await documentService.patchWithOptions(member, repositories, id, body);
72+
await actionItemService.postPatchAction(request, repositories, item);
73+
return item;
74+
});
75+
},
76+
);
77+
};
78+
79+
export default plugin;

0 commit comments

Comments
 (0)