@opentermsarchive/engine 11.0.2 → 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.
- package/.eslintrc.yaml +3 -0
- package/config/default.json +5 -0
- package/config/test.json +4 -1
- package/package.json +3 -2
- package/scripts/reporter/duplicate/index.js +1 -1
- package/src/archivist/collection/index.test.js +1 -1
- package/src/archivist/recorder/record.js +21 -0
- package/src/archivist/recorder/repositories/git/dataMapper.js +20 -11
- package/src/archivist/recorder/repositories/git/git.js +14 -2
- package/src/archivist/recorder/repositories/git/index.js +67 -11
- package/src/archivist/recorder/repositories/git/index.test.js +282 -6
- package/src/archivist/recorder/repositories/interface.js +44 -6
- package/src/archivist/recorder/repositories/mongo/index.js +67 -4
- package/src/archivist/recorder/repositories/mongo/index.test.js +330 -13
- package/src/archivist/recorder/version.test.js +38 -0
- package/src/archivist/services/index.js +1 -1
- package/src/collection-api/routes/feed.js +253 -0
- package/src/collection-api/routes/feed.test.js +739 -0
- package/src/collection-api/routes/index.js +16 -1
- package/src/collection-api/routes/services.js +1 -2
- package/src/collection-api/routes/services.test.js +4 -41
- package/src/collection-api/routes/versions.js +78 -78
- package/src/collection-api/routes/versions.test.js +4 -4
- package/src/collection-api/server.js +2 -0
- package/src/reporter/gitlab/index.js +1 -1
|
@@ -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
|
|
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('
|
|
108
|
+
context('when the service ID casing does not match', () => {
|
|
110
109
|
before(async () => {
|
|
111
|
-
response = await request(app).get(`${basePath}/v1/service/${encodeURI(
|
|
110
|
+
response = await request(app).get(`${basePath}/v1/service/${encodeURI(SERVICE_ID.toLowerCase())}`);
|
|
112
111
|
});
|
|
113
112
|
|
|
114
|
-
it('responds with
|
|
115
|
-
expect(response.status).to.equal(
|
|
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
|
-
|
|
30
|
+
export default function versionsRouter(versionsRepository) {
|
|
31
|
+
const router = express.Router();
|
|
31
32
|
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
98
|
+
const version = await versionsRepository.findByDate(serviceId, termsType, requestedDate);
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
if (!version) {
|
|
101
|
+
return res.status(404).json({ error: `No version found for date ${date}` });
|
|
102
|
+
}
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|