@opentermsarchive/engine 11.0.1 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,13 @@
1
+ import config from 'config';
1
2
  import express from 'express';
2
3
  import helmet from 'helmet';
3
4
 
4
5
  import { getCollection } from '../../archivist/collection/index.js';
6
+ import RepositoryFactory from '../../archivist/recorder/repositories/factory.js';
5
7
  import * as Services from '../../archivist/services/index.js';
6
8
 
7
9
  import docsRouter from './docs.js';
10
+ import feedRouter from './feed.js';
8
11
  import metadataRouter from './metadata.js';
9
12
  import servicesRouter from './services.js';
10
13
  import versionsRouter from './versions.js';
@@ -33,10 +36,22 @@ export default async function apiRouter(basePath) {
33
36
 
34
37
  const services = await Services.load();
35
38
  const collection = await getCollection();
39
+ const versionsStorageConfig = config.get('@opentermsarchive/engine.recorder.versions.storage');
40
+ const versionsRepository = await RepositoryFactory.create(versionsStorageConfig).initialize();
41
+ const feedConfig = config.get('@opentermsarchive/engine.collection-api.feed');
42
+
43
+ if (!collection.metadata?.id) {
44
+ throw new Error('Collection metadata "id" is required to expose feed endpoints, as it is used to build the tag URIs that uniquely identify the feed and its entries. Add an "id" field to the collection metadata file.');
45
+ }
46
+
47
+ if (!collection.metadata?.name) {
48
+ throw new Error('Collection metadata "name" is required to expose feed endpoints, as it is used as the Atom feed title which the Atom 1.0 specification requires to be non-empty. Add a "name" field to the collection metadata file.');
49
+ }
36
50
 
37
51
  router.use(await metadataRouter(collection, services));
38
52
  router.use(servicesRouter(services));
39
- router.use(versionsRouter);
53
+ router.use(versionsRouter(versionsRepository));
54
+ router.use(feedRouter(services, versionsRepository, versionsStorageConfig.type, feedConfig.limit, feedConfig.versionUrlTemplate));
40
55
 
41
56
  return router;
42
57
  }
@@ -130,8 +130,7 @@ export default function servicesRouter(services) {
130
130
  * description: No service matching the provided ID is found.
131
131
  */
132
132
  router.get('/service/:serviceId', (req, res) => {
133
- const matchedServiceID = Object.keys(services).find(key => key.toLowerCase() === req.params.serviceId?.toLowerCase());
134
- const service = services[matchedServiceID];
133
+ const service = Object.hasOwn(services, req.params.serviceId) ? services[req.params.serviceId] : null;
135
134
 
136
135
  if (!service) {
137
136
  res.status(404).send('Service not found');
@@ -56,7 +56,6 @@ describe('Services API', () => {
56
56
  describe('GET /service/:serviceId', () => {
57
57
  let response;
58
58
  const SERVICE_ID = 'Service B!';
59
- const CASE_INSENSITIVE_SERVICE_ID = 'service b!';
60
59
 
61
60
  before(async () => {
62
61
  response = await request(app).get(`${basePath}/v1/service/${encodeURI(SERVICE_ID)}`);
@@ -106,49 +105,13 @@ describe('Services API', () => {
106
105
  });
107
106
  });
108
107
 
109
- context('with a case-insensitive service ID parameter', () => {
108
+ context('when the service ID casing does not match', () => {
110
109
  before(async () => {
111
- response = await request(app).get(`${basePath}/v1/service/${encodeURI(CASE_INSENSITIVE_SERVICE_ID)}`);
110
+ response = await request(app).get(`${basePath}/v1/service/${encodeURI(SERVICE_ID.toLowerCase())}`);
112
111
  });
113
112
 
114
- it('responds with 200 status code', () => {
115
- expect(response.status).to.equal(200);
116
- });
117
-
118
- it('returns a service object with id', () => {
119
- expect(response.body).to.have.property('id');
120
- });
121
-
122
- it('returns the proper service object', () => {
123
- expect(response.body.id).to.equal(SERVICE_ID);
124
- });
125
-
126
- it('returns a service object with name', () => {
127
- expect(response.body).to.have.property('name');
128
- });
129
-
130
- it('returns a service object with an array of terms', () => {
131
- expect(response.body).to.have.property('terms').that.is.an('array');
132
- });
133
-
134
- it('each terms should have a type property', () => {
135
- response.body.terms.forEach(terms => {
136
- expect(terms).to.have.property('type');
137
- });
138
- });
139
-
140
- it('each terms should have an array of source documents', () => {
141
- response.body.terms.forEach(terms => {
142
- expect(terms).to.have.property('sourceDocuments').that.is.an('array');
143
- });
144
- });
145
-
146
- it('each source document should have a location', () => {
147
- response.body.terms.forEach(terms => {
148
- terms.sourceDocuments.forEach(sourceDocument => {
149
- expect(sourceDocument).to.have.property('location');
150
- });
151
- });
113
+ it('responds with 404 status code', () => {
114
+ expect(response.status).to.equal(404);
152
115
  });
153
116
  });
154
117
 
@@ -1,10 +1,10 @@
1
- import config from 'config';
2
1
  import express from 'express';
3
2
 
4
- import RepositoryFactory from '../../archivist/recorder/repositories/factory.js';
5
3
  import { toISODateWithoutMilliseconds } from '../../archivist/utils/date.js';
6
4
 
7
5
  /**
6
+ * @param {object} versionsRepository The versions repository instance
7
+ * @returns {express.Router} The router instance
8
8
  * @private
9
9
  * @swagger
10
10
  * tags:
@@ -27,86 +27,86 @@ import { toISODateWithoutMilliseconds } from '../../archivist/utils/date.js';
27
27
  * type: string
28
28
  * description: The JSON-escaped Markdown content of the version
29
29
  */
30
- const router = express.Router();
30
+ export default function versionsRouter(versionsRepository) {
31
+ const router = express.Router();
31
32
 
32
- const versionsRepository = await RepositoryFactory.create(config.get('@opentermsarchive/engine.recorder.versions.storage')).initialize();
33
+ /**
34
+ * @private
35
+ * @swagger
36
+ * /version/{serviceId}/{termsType}/{date}:
37
+ * get:
38
+ * summary: Get a specific version of some terms at a given date.
39
+ * tags: [Versions]
40
+ * produces:
41
+ * - application/json
42
+ * parameters:
43
+ * - in: path
44
+ * name: serviceId
45
+ * description: The ID of the service whose version will be returned.
46
+ * schema:
47
+ * type: string
48
+ * required: true
49
+ * - in: path
50
+ * name: termsType
51
+ * description: The type of terms whose version will be returned.
52
+ * schema:
53
+ * type: string
54
+ * required: true
55
+ * - in: path
56
+ * name: date
57
+ * description: The date and time for which the version is requested, in ISO 8601 format.
58
+ * schema:
59
+ * type: string
60
+ * format: date-time
61
+ * required: true
62
+ * responses:
63
+ * 200:
64
+ * description: A JSON object containing the version content and metadata.
65
+ * content:
66
+ * application/json:
67
+ * schema:
68
+ * $ref: '#/components/schemas/Version'
69
+ * 404:
70
+ * description: No version found for the specified combination of service ID, terms type and date.
71
+ * content:
72
+ * application/json:
73
+ * schema:
74
+ * type: object
75
+ * properties:
76
+ * error:
77
+ * type: string
78
+ * description: Error message indicating that no version is found.
79
+ * 416:
80
+ * description: The requested date is in the future.
81
+ * content:
82
+ * application/json:
83
+ * schema:
84
+ * type: object
85
+ * properties:
86
+ * error:
87
+ * type: string
88
+ * description: Error message indicating that the requested date is in the future.
89
+ */
90
+ router.get('/version/:serviceId/:termsType/:date', async (req, res) => {
91
+ const { serviceId, termsType, date } = req.params;
92
+ const requestedDate = new Date(date);
33
93
 
34
- /**
35
- * @private
36
- * @swagger
37
- * /version/{serviceId}/{termsType}/{date}:
38
- * get:
39
- * summary: Get a specific version of some terms at a given date.
40
- * tags: [Versions]
41
- * produces:
42
- * - application/json
43
- * parameters:
44
- * - in: path
45
- * name: serviceId
46
- * description: The ID of the service whose version will be returned.
47
- * schema:
48
- * type: string
49
- * required: true
50
- * - in: path
51
- * name: termsType
52
- * description: The type of terms whose version will be returned.
53
- * schema:
54
- * type: string
55
- * required: true
56
- * - in: path
57
- * name: date
58
- * description: The date and time for which the version is requested, in ISO 8601 format.
59
- * schema:
60
- * type: string
61
- * format: date-time
62
- * required: true
63
- * responses:
64
- * 200:
65
- * description: A JSON object containing the version content and metadata.
66
- * content:
67
- * application/json:
68
- * schema:
69
- * $ref: '#/components/schemas/Version'
70
- * 404:
71
- * description: No version found for the specified combination of service ID, terms type and date.
72
- * content:
73
- * application/json:
74
- * schema:
75
- * type: object
76
- * properties:
77
- * error:
78
- * type: string
79
- * description: Error message indicating that no version is found.
80
- * 416:
81
- * description: The requested date is in the future.
82
- * content:
83
- * application/json:
84
- * schema:
85
- * type: object
86
- * properties:
87
- * error:
88
- * type: string
89
- * description: Error message indicating that the requested date is in the future.
90
- */
91
- router.get('/version/:serviceId/:termsType/:date', async (req, res) => {
92
- const { serviceId, termsType, date } = req.params;
93
- const requestedDate = new Date(date);
94
-
95
- if (requestedDate > new Date()) {
96
- return res.status(416).json({ error: 'Requested version is in the future' });
97
- }
94
+ if (requestedDate > new Date()) {
95
+ return res.status(416).json({ error: 'Requested version is in the future' });
96
+ }
98
97
 
99
- const version = await versionsRepository.findByDate(serviceId, termsType, requestedDate);
98
+ const version = await versionsRepository.findByDate(serviceId, termsType, requestedDate);
100
99
 
101
- if (!version) {
102
- return res.status(404).json({ error: `No version found for date ${date}` });
103
- }
100
+ if (!version) {
101
+ return res.status(404).json({ error: `No version found for date ${date}` });
102
+ }
104
103
 
105
- return res.status(200).json({
106
- id: version.id,
107
- fetchDate: toISODateWithoutMilliseconds(version.fetchDate),
108
- content: version.content,
104
+ return res.status(200).json({
105
+ id: version.id,
106
+ fetchDate: toISODateWithoutMilliseconds(version.fetchDate),
107
+ content: version.content,
108
+ });
109
109
  });
110
- });
111
110
 
112
- export default router;
111
+ return router;
112
+ }
@@ -17,7 +17,7 @@ describe('Versions API', () => {
17
17
  let versionsRepository;
18
18
  const FETCH_DATE = new Date('2023-01-01T12:00:00Z');
19
19
  const VERSION_COMMON_ATTRIBUTES = {
20
- serviceId: 'service-1',
20
+ serviceId: 'service·A',
21
21
  termsType: 'Terms of Service',
22
22
  snapshotId: ['snapshot_id'],
23
23
  };
@@ -62,7 +62,7 @@ describe('Versions API', () => {
62
62
 
63
63
  context('when a version is found', () => {
64
64
  before(async () => {
65
- response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(FETCH_DATE))}`);
65
+ response = await request.get(`${basePath}/v1/version/service·A/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(FETCH_DATE))}`);
66
66
  });
67
67
 
68
68
  it('responds with 200 status code', () => {
@@ -80,7 +80,7 @@ describe('Versions API', () => {
80
80
 
81
81
  context('when the requested date is anterior to the first available version', () => {
82
82
  before(async () => {
83
- response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/2000-01-01T12:00:00Z`);
83
+ response = await request.get(`${basePath}/v1/version/service·A/Terms%20of%20Service/2000-01-01T12:00:00Z`);
84
84
  });
85
85
 
86
86
  it('responds with 404 status code', () => {
@@ -100,7 +100,7 @@ describe('Versions API', () => {
100
100
  before(async () => {
101
101
  const dateInTheFuture = new Date(Date.now() + 60000); // 1 minute in the future
102
102
 
103
- response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(dateInTheFuture))}`);
103
+ response = await request.get(`${basePath}/v1/version/service·A/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(dateInTheFuture))}`);
104
104
  });
105
105
 
106
106
  it('responds with 416 status code', () => {
@@ -8,6 +8,8 @@ import apiRouter from './routes/index.js';
8
8
 
9
9
  const app = express();
10
10
 
11
+ app.set('trust proxy', 'loopback'); // The API binds to 127.0.0.1 and is expected to run behind a reverse proxy. Honour X-Forwarded-* headers only when they come from a local proxy so absolute URLs emitted by routes (notably Atom feed links) reflect the URL seen by clients rather than the internal http://127.0.0.1 hop.
12
+
11
13
  if (process.env.NODE_ENV !== 'test') {
12
14
  app.use(loggerMiddleware);
13
15
  }
@@ -358,7 +358,7 @@ export default class GitLab {
358
358
  try {
359
359
  let apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?search=${encodeURIComponent(title)}&state=${searchParams.state}&per_page=100`;
360
360
 
361
- if (searchParams.state == 'all') apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?search=${encodeURIComponent(title)}&per_page=100`;
361
+ if (searchParams.state == 'all') { apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?search=${encodeURIComponent(title)}&per_page=100`; }
362
362
 
363
363
  const options = GitLab.baseOptionsHttpReq();
364
364