Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
de0c622
Install xml-js
clementbiron Apr 21, 2026
78ff82e
Add findRecent method
clementbiron Apr 21, 2026
18a5181
Add collection Atom feed endpoint
clementbiron Apr 22, 2026
e73d362
Add service-scoped feed endpoint
clementbiron Apr 22, 2026
891151a
Add service and terms type scoped feed endpoint
clementbiron Apr 22, 2026
54e0e1d
Cap feed entries with configurable limit
clementbiron Apr 22, 2026
eb24e39
Link feed entries to GitHub commits
clementbiron Apr 22, 2026
c21bb46
Resolve serviceId case-insensitively
clementbiron Apr 22, 2026
d2ed924
Add changelog entry
clementbiron Apr 22, 2026
a6a4723
Enforce consistent brace style
clementbiron Apr 29, 2026
2582940
Lint
clementbiron Apr 29, 2026
ef71d57
Instantiate versions repository in API router
clementbiron Apr 29, 2026
693dc28
Resolve serviceId case-sensitively
clementbiron Apr 29, 2026
793c0ab
Restructure repository query API with pagination
clementbiron Apr 29, 2026
b0af4e6
Improve code readbility
clementbiron May 4, 2026
64aa065
Factorize feed response
clementbiron May 4, 2026
1128a58
Inject feed limit, author and tag authority
clementbiron May 4, 2026
6da0199
Factor out buildFeedId helper
clementbiron May 4, 2026
68cbcff
Factor out buildSchemes helper
clementbiron May 4, 2026
7ae4e8d
Hardcode feed tag authority and author name
clementbiron May 4, 2026
e26b940
Replace feed dual links with URL template
clementbiron May 4, 2026
f653d08
Lint
Ndpnt May 12, 2026
7d27aa4
Add includeTechnicalUpgrades option in interface
Ndpnt May 12, 2026
a37d00e
Implement technical upgrades filtering in Git
Ndpnt May 12, 2026
b132cd4
Implement technical upgrades filtering in Mongo
Ndpnt May 12, 2026
9ea4e99
Exclude technical upgrades from feed endpoints
Ndpnt May 12, 2026
30794e2
Document versionUrlTemplate option in CHANGELOG
Ndpnt May 12, 2026
26cfb4a
Ensure feed test isolation
Ndpnt May 12, 2026
380af24
Fail fast if collection metadata id is missing
Ndpnt May 12, 2026
379fd43
Document TAG_AUTHORITY year per RFC 4151
Ndpnt May 12, 2026
ceb2b90
Fix funder
Ndpnt May 12, 2026
5c3da56
Add displayTitle getter on Record class
Ndpnt May 12, 2026
ad3b571
Use displayTitle for git commit subjects
Ndpnt May 12, 2026
78865b5
Use displayTitle for feed entry titles
Ndpnt May 12, 2026
1f1b72a
Honour reverse proxy headers in feed URLs
Ndpnt May 12, 2026
5b42fdf
Hoist feed schemes to a module-level constant
Ndpnt May 12, 2026
c29a2c4
Require collection name to expose feed endpoints
Ndpnt May 12, 2026
186ead3
Skip Atom subtitle when collection has no tagline
Ndpnt May 12, 2026
260e00f
Use accurate MIME types on feed links
Ndpnt May 12, 2026
dd0ddd8
Use a stable updated date for empty feeds
Ndpnt May 12, 2026
a2b86a8
Enable conditional GET on feeds via Last-Modified
Ndpnt May 12, 2026
b6ed98f
Escape XML special characters in feed attributes
Ndpnt May 12, 2026
9f371fb
Apply XML attribute escape via js2xml hook
Ndpnt May 12, 2026
f2eea3a
Annotate listCommits git log options inline
Ndpnt May 12, 2026
58637aa
Express commit prefix groups as disjoint sets
Ndpnt May 12, 2026
f8a90d2
Improve comments
Ndpnt May 13, 2026
6c566cb
Factor feed rendering into a renderFeed helper
Ndpnt May 13, 2026
c857bc2
Add findByService tests
Ndpnt May 13, 2026
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
3 changes: 3 additions & 0 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ rules:
- error
- always-multiline
consistent-return: 0
curly:
- error
- all
function-paren-newline:
- error
- multiline
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

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 [major]

> Development of this release was supported by the [NGI0 Commons Fund](https://nlnet.nl/project/Modular-OTA/), 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

- Add `GET /feed` endpoint on the Collection API exposing an Atom feed of the latest version changes across the whole collection
- Add `GET /feed/:serviceId` endpoint on the Collection API exposing an Atom feed scoped to a single service
- Add `GET /feed/:serviceId/:termsType` endpoint on the Collection API exposing an Atom feed scoped to a single service and terms type
- Add [`@opentermsarchive/engine.collection-api.feed.limit`](https://docs.opentermsarchive.org/collections/reference/configuration/) configuration option controlling the maximum number of entries returned by feed endpoints (default: `100`)
- Add [`@opentermsarchive/engine.collection-api.feed.versionUrlTemplate`](https://docs.opentermsarchive.org/collections/reference/configuration/) configuration option to customize the `alternate` link of feed entries with a URL template (e.g. `https://github.com/openTermsArchive/demo-versions/commit/%VERSION_ID`); useful to point feed readers to a human-readable page instead of the default version API endpoint

### Changed

- **Breaking:** Resolve `serviceId` path parameter case-sensitively on the `GET /service/:serviceId` endpoint, in line with the documented service ID format; clients relying on case-insensitive matching must now use the exact ID casing

## 11.0.2 - 2026-04-14

> Development of this release was supported by [Reset Tech](https://www.reset.tech).
Expand Down
5 changes: 5 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
},
"dataset": {
"publishingSchedule": "30 8 * * MON"
},
"collection-api": {
"feed": {
"limit": 100
}
}
}
}
5 changes: 4 additions & 1 deletion config/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
},
"collection-api": {
"port": 3000,
"basePath": "/collection-api"
"basePath": "/collection-api",
"feed": {
"limit": 3
}
}
}
}
33 changes: 16 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@
"swagger-ui-express": "^5.0.1",
"turndown": "^7.2.1",
"winston": "^3.17.0",
"winston-mail": "^2.0.0"
"winston-mail": "^2.0.0",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
Expand Down
2 changes: 1 addition & 1 deletion scripts/reporter/duplicate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function removeDuplicateIssues() {
}

for (const [ title, duplicateIssues ] of issuesByTitle) {
if (duplicateIssues.length === 1) continue;
if (duplicateIssues.length === 1) { continue; }

const originalIssue = duplicateIssues.reduce((oldest, current) => (new Date(current.created_at) < new Date(oldest.created_at) ? current : oldest));

Expand Down
2 changes: 1 addition & 1 deletion src/archivist/collection/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Collection', () => {
try {
metadataBackup = await fs.readFile(metadataPath, 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') throw error;
if (error.code !== 'ENOENT') { throw error; }
}
});

Expand Down
21 changes: 21 additions & 0 deletions src/archivist/recorder/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
* @class Record
* @private
*/

export const TITLE_PREFIXES = Object.freeze({
firstRecord: 'First record of',
technicalUpgrade: 'Apply technical or declaration upgrade on',
update: 'Record new changes of',
});

export default class Record {
#content;

Expand Down Expand Up @@ -32,6 +39,20 @@ export default class Record {
this.#content = content;
}

get displayTitle() {
let prefix;

if (this.isFirstRecord) {
prefix = TITLE_PREFIXES.firstRecord;
} else if (this.isTechnicalUpgrade) {
prefix = TITLE_PREFIXES.technicalUpgrade;
} else {
prefix = TITLE_PREFIXES.update;
}

return `${prefix} ${this.serviceId} ${this.termsType}`;
}

validate() {
for (const requiredParam of this.constructor.REQUIRED_PARAMS) {
if (requiredParam == 'content') {
Expand Down
31 changes: 20 additions & 11 deletions src/archivist/recorder/repositories/git/dataMapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import path from 'path';

import mime from 'mime';

import { TITLE_PREFIXES } from '../../record.js';
import Snapshot from '../../snapshot.js';
import Version from '../../version.js';

export const COMMIT_MESSAGE_PREFIXES = {
startTracking: 'First record of',
technicalUpgrade: 'Apply technical or declaration upgrade on',
update: 'Record new changes of',
// Prefixes for commits that represent an actual content change detected at the service source
const CHANGE_PREFIXES = {
startTracking: TITLE_PREFIXES.firstRecord,
update: TITLE_PREFIXES.update,
deprecated_startTracking: 'Start tracking',
deprecated_refilter: 'Refilter',
deprecated_update: 'Update',
};

// Prefixes for commits that re-render an existing snapshot (e.g. with updated extraction rules) without any change at the service source
const TECHNICAL_UPGRADE_PREFIXES = {
technicalUpgrade: TITLE_PREFIXES.technicalUpgrade,
deprecated_refilter: 'Refilter',
};

export const CHANGE_COMMIT_MESSAGE_PREFIXES = CHANGE_PREFIXES;
export const COMMIT_MESSAGE_PREFIXES = { ...CHANGE_PREFIXES, ...TECHNICAL_UPGRADE_PREFIXES };

export const TERMS_TYPE_AND_DOCUMENT_ID_SEPARATOR = ' #';
export const SNAPSHOT_ID_MARKER = '%SNAPSHOT_ID';
const SINGLE_SOURCE_DOCUMENT_PREFIX = 'This version was recorded after extracting from snapshot';
Expand All @@ -22,13 +31,9 @@ const MULTIPLE_SOURCE_DOCUMENTS_PREFIX = 'This version was recorded after extrac
export const COMMIT_MESSAGE_PREFIXES_REGEXP = new RegExp(`^(${Object.values(COMMIT_MESSAGE_PREFIXES).join('|')})`);

export function toPersistence(record, snapshotIdentiferTemplate) {
const { serviceId, termsType, documentId, isTechnicalUpgrade, snapshotIds = [], mimeType, isFirstRecord, metadata } = record;
const { serviceId, termsType, documentId, snapshotIds = [], mimeType, metadata } = record;

let prefix = isTechnicalUpgrade ? COMMIT_MESSAGE_PREFIXES.technicalUpgrade : COMMIT_MESSAGE_PREFIXES.update;

prefix = isFirstRecord ? COMMIT_MESSAGE_PREFIXES.startTracking : prefix;

const subject = `${prefix} ${serviceId} ${termsType}`;
const subject = record.displayTitle;
const documentIdMessage = `${documentId ? `Document ID ${documentId}\n\n` : ''}`;
let snapshotIdsMessage;

Expand Down Expand Up @@ -91,6 +96,10 @@ function generateFileName(termsType, documentId, extension) {
}

export function generateFilePath(serviceId, termsType, documentId, mimeType) {
if (termsType === undefined) {
return `${serviceId}/*`; // If only serviceId is provided, return a pattern to match all files for that service
}

const extension = mime.getExtension(mimeType) || '*'; // If mime type is undefined, an asterisk is set as an extension. Used to match all files for the given service ID, terms type and document ID when mime type is unknown

return `${serviceId}/${generateFileName(termsType, documentId, extension)}`; // Do not use `path.join` as even for Windows, the path should be with `/` and not `\`
Expand Down
16 changes: 14 additions & 2 deletions src/archivist/recorder/repositories/git/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,20 @@ export default class Git {
return this.git.push();
}

listCommits(options = []) {
return this.log([ '--reverse', '--no-merges', '--name-only', ...options ]); // Returns all commits in chronological order (`--reverse`), excluding merge commits (`--no-merges`), with modified files names (`--name-only`)
listCommits(options = [], { reverse = true, skip, maxCount } = {}) {
const reverseOption = reverse ? ['--reverse'] : [];
const skipOption = skip !== undefined ? [`--skip=${skip}`] : [];
const maxCountOption = maxCount !== undefined ? [`--max-count=${maxCount}`] : [];

return this.log([
...reverseOption, // When `reverse` is true, lists commits oldest-first; otherwise the default newest-first applies
'--author-date-order', // Best-effort author-date ordering: with --max-count, git applies the cap topologically, so the page can miss strictly-newer commits that #getCommits' JS resort cannot recover
'--no-merges', // Exclude merge commits — records are stored as regular commits, never as merges
'--name-only', // Append the modified file names below each commit, used by `toDomain` to extract the record's file path
...skipOption, // Optional `--skip=N`: drop the first N matching commits (pagination offset)
...maxCountOption, // Optional `--max-count=N`: cap the result to N commits (pagination limit)
...options, // Caller-supplied options: typically grep filters on commit messages and a path filter (`-- pathspec`)
]);
}

async getCommit(options) {
Expand Down
Loading
Loading