Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a dedicated route in the Version API to obtain the latest version of some terms #1034

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## Unreleased [minor]

_Full changeset and discussions: [#1034](https://github.com/OpenTermsArchive/engine/pull/1034)._

> Development of this release was [supported](https://nlnet.nl/project/TOSDR-OTA/) by the [NGI0 Entrust Fund](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://www.ngi.eu) programme, under the aegis of DG CNECT under grant agreement N°101069594.

### Added

- Provide a dedicated route in the Version API to obtain the latest version of some terms ([#1028](https://github.com/OpenTermsArchive/engine/pull/1028#issuecomment-1847188776))

## 0.34.0 - 2023-12-11

Expand All @@ -12,7 +20,7 @@ _Full changeset and discussions: [#1033](https://github.com/OpenTermsArchive/eng

### Added

- Expose versions data through the collection API ([#1003](https://github.com/OpenTermsArchive/engine/pull/1028)). When using Git as storage for versions, this API relies on the assumption that the commit date matches the author date, introduced in the engine in June 2022 ([#875](https://github.com/OpenTermsArchive/engine/pull/875)). If your collection was created before this date, inconsistencies in the API results may arise. You can verify if your version history includes commits with commit dates differing from author dates by executing the following command at the root of your versions repository: `git log --format="%H %ad %cI" --date=iso-strict | awk '{if ($2 != $3) print "Author date", $2, "and commit date", $3, "mismatch for commit", $1 }'`. You can correct the history with the command: `git rebase --committer-date-is-author-date $(git rev-list --max-parents=0 HEAD)`. Since the entire history will be rewritten, a force push may be required for distributed repositories
- Expose versions data through the collection API ([#1028](https://github.com/OpenTermsArchive/engine/pull/1028)). When using Git as storage for versions, this API relies on the assumption that the commit date matches the author date, introduced in the engine in June 2022 ([#875](https://github.com/OpenTermsArchive/engine/pull/875)). If your collection was created before this date, inconsistencies in the API results may arise. You can verify if your version history includes commits with commit dates differing from author dates by executing the following command at the root of your versions repository: `git log --format="%H %ad %cI" --date=iso-strict | awk '{if ($2 != $3) print "Author date", $2, "and commit date", $3, "mismatch for commit", $1 }'`. You can correct the history with the command: `git rebase --committer-date-is-author-date $(git rev-list --max-parents=0 HEAD)`. Since the entire history will be rewritten, a force push may be required for distributed repositories

### Changed

Expand Down
63 changes: 59 additions & 4 deletions src/api/routes/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,57 @@ const router = express.Router();

const versionsRepository = await RepositoryFactory.create(config.get('recorder.versions.storage')).initialize();

/**
* @swagger
* /version/{serviceId}/{termsType}/latest:
* get:
* summary: Get the latest version of some terms.
* tags: [Versions]
* produces:
* - application/json
* parameters:
* - in: path
* name: serviceId
* description: The ID of the service whose version will be returned.
* schema:
* type: string
* required: true
* - in: path
* name: termsType
* description: The type of terms whose version will be returned.
* schema:
* type: string
* required: true
* responses:
* 200:
* description: A JSON object containing the version content and metadata.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Version'
* 404:
* description: No version found for the specified combination of service ID and terms type.
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* description: Error message indicating that no version is found.
*/
router.get('/version/:serviceId/:termsType/latest', async (req, res) => {
const { serviceId, termsType } = req.params;

const version = await versionsRepository.findLatest(serviceId, termsType);

if (!version) {
return res.status(404).json({ error: `No version found for the specified combination of service ID "${serviceId}" and terms type "${termsType}"` });
}

return res.status(200).json(toJSON(version));
});

/**
* @swagger
* /version/{serviceId}/{termsType}/{date}:
Expand Down Expand Up @@ -97,14 +148,18 @@ router.get('/version/:serviceId/:termsType/:date', async (req, res) => {
const version = await versionsRepository.findByDate(serviceId, termsType, requestedDate);

if (!version) {
return res.status(404).json({ error: `No version found for date ${date}` });
return res.status(404).json({ error: `No version found the specified combination of service ID "${serviceId}", terms type "${termsType}" and date ${date}` });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return res.status(404).json({ error: `No version found the specified combination of service ID "${serviceId}", terms type "${termsType}" and date ${date}` });
return res.status(404).json({ error: `No version found for the specified combination of service ID "${serviceId}", terms type "${termsType}" and date ${date}` });

}

return res.status(200).json({
return res.status(200).json(toJSON(version));
});

function toJSON(version) {
return {
id: version.id,
fetchDate: toISODateWithoutMilliseconds(version.fetchDate),
content: version.content,
});
});
};
}

export default router;
153 changes: 109 additions & 44 deletions src/api/routes/versions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,56 +13,121 @@ const { expect } = chai;
const request = supertest(app);

describe('Versions API', () => {
describe('GET /version/:serviceId/:termsType/:date', () => {
let expectedResult;
let versionsRepository;
const FETCH_DATE = new Date('2023-01-01T12:00:00Z');
const VERSION_COMMON_ATTRIBUTES = {
serviceId: 'service-1',
termsType: 'Terms of Service',
snapshotId: ['snapshot_id'],
};

before(async () => {
versionsRepository = RepositoryFactory.create(config.get('recorder.versions.storage'));

await versionsRepository.initialize();

const ONE_HOUR = 60 * 60 * 1000;

await versionsRepository.save(new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'initial content',
fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR),
}));

const version = new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'updated content',
fetchDate: FETCH_DATE,
});

await versionsRepository.save(version);

await versionsRepository.save(new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'latest content',
fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR),
}));

expectedResult = {
id: version.id,
fetchDate: toISODateWithoutMilliseconds(version.fetchDate),
content: version.content,
};
let expectedResult;
let versionsRepository;
let latestVersion;
let version;

const FETCH_DATE = new Date('2023-01-01T12:00:00Z');
const VERSION_COMMON_ATTRIBUTES = {
serviceId: 'service-1',
termsType: 'Terms of Service',
snapshotId: ['snapshot_id'],
};

let response;

before(async () => {
versionsRepository = RepositoryFactory.create(config.get('recorder.versions.storage'));

await versionsRepository.initialize();

const ONE_HOUR = 60 * 60 * 1000;

await versionsRepository.save(new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'initial content',
fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR),
}));

version = new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'updated content',
fetchDate: FETCH_DATE,
});

after(async () => versionsRepository.removeAll());
await versionsRepository.save(version);

let response;
latestVersion = new Version({
...VERSION_COMMON_ATTRIBUTES,
content: 'latest content',
fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR),
});

await versionsRepository.save(latestVersion);
});

after(async () => versionsRepository.removeAll());

describe('GET /version/:serviceId/:termsType/latest', () => {
context('when a version is found', () => {
before(async () => {
expectedResult = {
id: latestVersion.id,
fetchDate: toISODateWithoutMilliseconds(latestVersion.fetchDate),
content: latestVersion.content,
};
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/latest`);
});

it('responds with 200 status code', () => {
expect(response.status).to.equal(200);
});

it('responds with Content-Type application/json', () => {
expect(response.type).to.equal('application/json');
});

it('returns the expected version', () => {
expect(response.body).to.deep.equal(expectedResult);
});
});

context('when the requested service does not exist', () => {
before(async () => {
response = await request.get(`${basePath}/v1/version/unknown-service/Terms%20of%20Service/latest`);
});

it('responds with 404 status code', () => {
expect(response.status).to.equal(404);
});

it('responds with Content-Type application/json', () => {
expect(response.type).to.equal('application/json');
});

it('returns an error message', () => {
expect(response.body.error).to.contain('No version found').and.to.contain('unknown-service');
});
});

context('when the requested terms type does not exist for the given service', () => {
before(async () => {
response = await request.get(`${basePath}/v1/version/service-1/Unknown%20Type/latest`);
});

it('responds with 404 status code', () => {
expect(response.status).to.equal(404);
});

it('responds with Content-Type application/json', () => {
expect(response.type).to.equal('application/json');
});

it('returns an error message', () => {
expect(response.body.error).to.contain('No version found').and.to.contain('Unknown Type');
});
});
});

describe('GET /version/:serviceId/:termsType/:date', () => {
context('when a version is found', () => {
before(async () => {
expectedResult = {
id: version.id,
fetchDate: toISODateWithoutMilliseconds(version.fetchDate),
content: version.content,
};
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(FETCH_DATE))}`);
});

Expand Down
Loading