• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 25753937432

12 May 2026 09:17AM UTC coverage: 88.117% (-1.0%) from 89.089%
25753937432

push

github

web-flow
chore(deps): fix missing dependencies for running docs:dev (#911)

3925 of 4574 branches covered (85.81%)

Branch coverage included in aggregate %.

20093 of 22683 relevant lines covered (88.58%)

1125.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.72
/src/controller/container-controller.ts
1
/**
1✔
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
1✔
20

21
/**
1✔
22
 * This is the module page of container-controller.
23
 *
24
 * @module catalogue/containers
25
 */
1✔
26

27
import log4js, { Logger } from 'log4js';
28
import { Response } from 'express';
29
import BaseController, { BaseControllerOptions } from './base-controller';
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import ContainerService from '../service/container-service';
33
import ContainerRevision from '../entity/container/container-revision';
34
import Container from '../entity/container/container';
35
import { asNumber } from '../helpers/validators';
36
import { parseRequestPagination, toResponse } from '../helpers/pagination';
37
import { baseContainerRequestSpec, createContainerRequestSpec } from './request/validators/container-request-spec';
38
import { globalAsyncValidatorRegistry } from '../middleware/async-validator-registry';
39
import {
40
  CreateContainerParams,
41
  CreateContainerRequest,
42
  UpdateContainerParams,
43
  UpdateContainerRequest,
44
} from './request/container-request';
45
import userTokenInOrgan from '../helpers/token-helper';
46

47
/**
1✔
48
 * Controller for managing all routes related to the `container` entity.
49
 */
1✔
50
export default class ContainerController extends BaseController {
1✔
51
  private logger: Logger = log4js.getLogger('ContainerController');
1✔
52

53
  /**
1✔
54
   * Creates a new product controller instance.
55
   * @param options - The options passed to the base controller.
56
   */
1✔
57
  public constructor(options: BaseControllerOptions) {
1✔
58
    super(options);
4✔
59
    this.configureLogger(this.logger);
4✔
60
    globalAsyncValidatorRegistry.register('CreateContainerRequest', createContainerRequestSpec);
4✔
61
    globalAsyncValidatorRegistry.register('UpdateContainerRequest', baseContainerRequestSpec);
4✔
62
  }
4✔
63

64
  /**
1✔
65
   * @inheritdoc
66
   */
1✔
67
  getPolicy(): Policy {
1✔
68
    return {
4✔
69
      '/': {
4✔
70
        GET: {
4✔
71
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Container', ['*']),
4✔
72
          handler: this.getAllContainers.bind(this),
4✔
73
        },
4✔
74
        POST: {
4✔
75
          body: { modelName: 'CreateContainerRequest' },
4✔
76
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'own', 'Container', ['*']),
4✔
77
          handler: this.createContainer.bind(this),
4✔
78
        },
4✔
79
      },
4✔
80
      '/:id(\\d+)': {
4✔
81
        GET: {
4✔
82
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await ContainerController.getRelation(req), 'Container', ['*']),
4✔
83
          handler: this.getSingleContainer.bind(this),
4✔
84
        },
4✔
85
        PATCH: {
4✔
86
          body: { modelName: 'UpdateContainerRequest' },
4✔
87
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', await ContainerController.getRelation(req), 'Container', ['*']),
4✔
88
          handler: this.updateContainer.bind(this),
4✔
89
        },
4✔
90
        DELETE: {
4✔
91
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', await ContainerController.getRelation(req), 'Container', ['*']),
4✔
92
          handler: this.deleteContainer.bind(this),
4✔
93
        },
4✔
94
      },
4✔
95
      '/:id(\\d+)/products': {
4✔
96
        GET: {
4✔
97
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await ContainerController.getRelation(req), 'Container', ['*']),
4✔
98
          handler: this.getProductsContainer.bind(this),
4✔
99
        },
4✔
100
      },
4✔
101
      '/public': {
4✔
102
        GET: {
4✔
103
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'public', 'Container', ['*']),
4✔
104
          handler: this.getPublicContainers.bind(this),
4✔
105
        },
4✔
106
      },
4✔
107
    };
4✔
108
  }
4✔
109

110
  /**
1✔
111
   * GET /containers
112
   * @summary Returns all existing containers
113
   * @operationId getAllContainers
114
   * @tags containers - Operations of container controller
115
   * @security JWT
116
   * @param {integer} take.query - How many containers the endpoint should return
117
   * @param {integer} skip.query - How many containers should be skipped (for pagination)
118
   * @return {PaginatedContainerResponse} 200 - All existing containers
119
   * @return {string} 500 - Internal server error
120
   */
1✔
121
  public async getAllContainers(req: RequestWithToken, res: Response): Promise<void> {
1✔
122
    const { body } = req;
5✔
123
    this.logger.trace('Get all containers', body, 'by user', req.token.user);
5✔
124

125
    const { take, skip } = parseRequestPagination(req);
5✔
126

127
    // Handle request
5✔
128
    try {
5✔
129
      const [revisions, count] = await ContainerService.getContainers(
5✔
130
        {}, { take, skip },
5✔
131
      );
132
      const records = revisions.map((r) => ContainerService.revisionToResponse(r));
5✔
133
      res.json(toResponse(records, count, { take, skip }));
5✔
134
    } catch (error) {
5!
135
      this.logger.error('Could not return all containers:', error);
×
136
      res.status(500).json('Internal server error.');
×
137
    }
×
138
  }
5✔
139

140
  /**
1✔
141
   * GET /containers/{id}
142
   * @summary Returns the requested container
143
   * @operationId getSingleContainer
144
   * @tags containers - Operations of container controller
145
   * @param {integer} id.path.required - The id of the container which should be returned
146
   * @security JWT
147
   * @return {ContainerWithProductsResponse} 200 - The requested container
148
   * @return {string} 404 - Not found error
149
   * @return {string} 403 - Incorrect permissions
150
   * @return {string} 500 - Internal server error
151
   */
1✔
152
  public async getSingleContainer(req: RequestWithToken, res: Response): Promise<void> {
1✔
153
    const { id } = req.params;
5✔
154
    this.logger.trace('Get single container', id, 'by user', req.token.user);
5✔
155

156
    const containerId = parseInt(id, 10);
5✔
157

158
    // Handle request
5✔
159
    try {
5✔
160
      const [revisions] = await ContainerService
5✔
161
        .getContainers({ containerId, returnProducts: true });
5✔
162
      if (!revisions.length) {
5✔
163
        res.status(404).json('Container not found.');
2✔
164
        return;
2✔
165
      }
2✔
166
      res.json(ContainerService.revisionToResponse(revisions[0]));
3✔
167
    } catch (error) {
5!
168
      this.logger.error('Could not return single container:', error);
×
169
      res.status(500).json('Internal server error.');
×
170
    }
×
171
  }
5✔
172

173
  /**
1✔
174
   * GET /containers/{id}/products
175
   * @summary Returns all the products in the container
176
   * @operationId getProductsContainer
177
   * @tags containers - Operations of container controller
178
   * @param {integer} id.path.required - The id of the container which should be returned
179
   * @security JWT
180
   * @return {Array.<ProductResponse>} 200 - All products in the container
181
   * @return {string} 404 - Not found error
182
   * @return {string} 500 - Internal server error
183
   */
1✔
184
  public async getProductsContainer(req: RequestWithToken, res: Response): Promise<void> {
1✔
185
    const { id } = req.params;
3✔
186
    const containerId = parseInt(id, 10);
3✔
187

188
    this.logger.trace('Get all products in container', containerId, 'by user', req.token.user);
3✔
189

190
    try {
3✔
191
      // Check if we should return a 404.
3✔
192
      const exist = await ContainerRevision.findOne({ where: { container: { id: containerId } } });
3✔
193
      if (!exist) {
3!
194
        res.status(404).json('Container not found.');
×
195
        return;
×
196
      }
×
197

198
      const revision = await ContainerService.getSingleContainer({ containerId, returnProducts: true });
3✔
199
      const response = revision ? ContainerService.revisionToResponse(revision) : undefined;
3!
200
      const products = response && 'products' in response ? response.products : [];
3!
201
      res.json(products);
3✔
202
    } catch (error) {
3!
203
      this.logger.error('Could not return all products in container:', error);
×
204
      res.status(500).json('Internal server error.');
×
205
    }
×
206
  }
3✔
207

208
  /**
1✔
209
   * POST /containers
210
   * @summary Create a new container.
211
   * @operationId createContainer
212
   * @tags containers - Operations of container controller
213
   * @param {CreateContainerRequest} request.body.required -
214
   *    The container which should be created
215
   * @security JWT
216
   * @return {ContainerWithProductsResponse} 200 - The created container entity
217
   * @return {ValidationResponse} 400 - Validation error
218
   * @return {string} 500 - Internal server error
219
   */
1✔
220
  public async createContainer(req: RequestWithToken, res: Response): Promise<void> {
1✔
221
    const body = req.body as CreateContainerRequest;
1✔
222
    this.logger.trace('Create container', body, 'by user', req.token.user);
1✔
223

224
    // handle request
1✔
225
    try {
1✔
226
      const request: CreateContainerParams = {
1✔
227
        ...body,
1✔
228
        ownerId: body.ownerId,
1✔
229
      };
1✔
230

231
      const revision = await ContainerService.createContainer(request);
1✔
232
      res.json(ContainerService.revisionToResponse(revision));
1✔
233
    } catch (error) {
1!
234
      this.logger.error('Could not create container:', error);
×
235
      res.status(500).json('Internal server error.');
×
236
    }
×
237
  }
1✔
238

239
  /**
1✔
240
   * GET /containers/public
241
   * @summary Returns all public container
242
   * @operationId getPublicContainers
243
   * @tags containers - Operations of container controller
244
   * @security JWT
245
   * @param {integer} take.query - How many containers the endpoint should return
246
   * @param {integer} skip.query - How many containers should be skipped (for pagination)
247
   * @return {PaginatedContainerResponse} 200 - All existing public containers
248
   * @return {string} 500 - Internal server error
249
   */
1✔
250
  public async getPublicContainers(req: RequestWithToken, res: Response): Promise<void> {
1✔
251
    const { body } = req;
2✔
252
    this.logger.trace('Get all public containers', body, 'by user', req.token.user);
2✔
253

254
    const { take, skip } = parseRequestPagination(req);
2✔
255

256
    // Handle request
2✔
257
    try {
2✔
258
      const [revisions, count] = await ContainerService.getContainers(
2✔
259
        { public: true }, { take, skip },
2✔
260
      );
261
      const records = revisions.map((r) => ContainerService.revisionToResponse(r));
2✔
262
      res.json(toResponse(records, count, { take, skip }));
2✔
263
    } catch (error) {
2!
264
      this.logger.error('Could not return all public containers:', error);
×
265
      res.status(500).json('Internal server error.');
×
266
    }
×
267
  }
2✔
268

269
  /**
1✔
270
   * PATCH /containers/{id}
271
   * @summary Update an existing container.
272
   * @operationId updateContainer
273
   * @tags containers - Operations of container controller
274
   * @param {integer} id.path.required - The id of the container which should be updated
275
   * @param {UpdateContainerRequest} request.body.required -
276
   *    The container which should be updated
277
   * @security JWT
278
   * @return {ContainerWithProductsResponse} 200 - The created container entity
279
   * @return {ValidationResponse} 400 - Validation error
280
   * @return {string} 404 - Product not found error
281
   * @return {string} 500 - Internal server error
282
   */
1✔
283
  public async updateContainer(req: RequestWithToken, res: Response): Promise<void> {
1✔
284
    const body = req.body as UpdateContainerRequest;
6✔
285
    const { id } = req.params;
6✔
286
    const containerId = Number.parseInt(id, 10);
6✔
287
    this.logger.trace('Update container', id, 'with', body, 'by user', req.token.user);
6✔
288

289
    // handle request
6✔
290
    try {
6✔
291
      const request: UpdateContainerParams = {
6✔
292
        ...body,
6✔
293
        id: containerId,
6✔
294
      };
6✔
295

296
      const container = await Container.findOne({ where: { id: containerId } });
6✔
297
      if (!container) {
6✔
298
        res.status(404).json('Container not found.');
1✔
299
        return;
1✔
300
      }
1✔
301

302
      const revision = await ContainerService.updateContainer(request);
5✔
303
      res.json(ContainerService.revisionToResponse(revision));
5✔
304
    } catch (error) {
6!
305
      this.logger.error('Could not update container:', error);
×
306
      res.status(500).json('Internal server error.');
×
307
    }
×
308
  }
6✔
309

310
  /**
1✔
311
   * DELETE /containers/{id}
312
   * @summary (Soft) delete the given container. Cannot be undone.
313
   * @operationId deleteContainer
314
   * @tags containers - Operations of container controller
315
   * @param {integer} id.path.required - The id of the container which should be deleted
316
   * @security JWT
317
   * @return {string} 204 - Success
318
   * @return {string} 404 - Not found error
319
   * @return {string} 500 - Internal server error
320
   */
1✔
321
  public async deleteContainer(req: RequestWithToken, res: Response): Promise<void> {
1✔
322
    const { id } = req.params;
3✔
323
    this.logger.trace('Delete container', id, 'by user', req.token.user);
3✔
324

325
    try {
3✔
326
      const containerId = parseInt(id, 10);
3✔
327

328
      const container = await Container.findOne({ where: { id: containerId } });
3✔
329

330
      if (container == null) {
3✔
331
        res.status(404).json('Container not found');
2✔
332
        return;
2✔
333
      }
2✔
334

335
      await ContainerService.deleteContainer(containerId);
1✔
336
      res.status(204).send();
1✔
337
      return;
1✔
338
    } catch (error) {
3!
339
      this.logger.error('Could not delete container', error);
×
340
      res.status(500).json('Internal server error.');
×
341
    }
×
342
  }
3✔
343

344
  /**
1✔
345
   * Function to determine which credentials are needed to get container
346
   *          'all' if user is not connected to container
347
   *          'organ' if user is not connected to container via organ
348
   *          'own' if user is connected to container
349
   * @param req
350
   * @return whether container is connected to used token
351
   */
1✔
352
  static async getRelation(req: RequestWithToken): Promise<string> {
1✔
353
    const containerId = asNumber(req.params.id);
28✔
354
    const container: Container = await Container.findOne({ where: { id: containerId }, relations: ['owner'] });
28✔
355

356
    if (!container) return 'all';
28✔
357
    if (userTokenInOrgan(req, container.owner.id)) return 'organ';
28✔
358

359
    const containerVisibility = await ContainerService.canViewContainer(
22✔
360
      req.token.user.id, container,
22✔
361
    );
362
    if (containerVisibility.own) return 'own';
28✔
363
    if (containerVisibility.public) return 'public';
27✔
364
    return 'all';
4✔
365
  }
4✔
366
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc