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

Expose collection metadata through the collection API #1123

Merged
merged 9 commits into from
Dec 4, 2024
Merged
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: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

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 [French Ministry for Foreign Affairs](https://www.diplomatie.gouv.fr/fr/politique-etrangere-de-la-france/diplomatie-numerique/) through its ministerial [State Startups incubator](https://beta.gouv.fr/startups/open-terms-archive.html) under the aegis of the Ambassador for Digital Affairs.

### Added

- Expose collection metadata through the collection API; requires a [metadata file](https://docs.opentermsarchive.org/collections/metadata/) at the root of your collection folder

### Changed

- **Breaking:** Replace `@opentermsarchive/engine.services.declarationsPath` with `@opentermsarchive/engine.collectionPath`; ensure your declarations are located in `./declarations` in your collection folder

## 3.0.0 - 2024-12-03

_Full changeset and discussions: [#1122](https://github.com/OpenTermsArchive/engine/pull/1122)._
Expand Down
4 changes: 1 addition & 3 deletions config/ci.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"@opentermsarchive/engine": {
"services": {
"declarationsPath": "./demo-declarations/declarations"
}
"collectionPath": "./demo-declarations/"
}
}
4 changes: 1 addition & 3 deletions config/default.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"@opentermsarchive/engine": {
"trackingSchedule": "30 */12 * * *",
"services": {
"declarationsPath": "./declarations"
},
"collectionPath": "./",
"recorder": {
"versions": {
"storage": {
Expand Down
4 changes: 1 addition & 3 deletions config/test.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{
"@opentermsarchive/engine": {
"services": {
"declarationsPath": "./test/services"
},
"collectionPath": "./test/test-declarations",
"recorder": {
"versions": {
"storage": {
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"https-proxy-agent": "^5.0.0",
"iconv-lite": "^0.6.3",
"joplin-turndown-plugin-gfm": "^1.0.12",
"js-yaml": "^4.1.0",
"jsdom": "^18.1.0",
"json-source-map": "^0.6.1",
"lodash": "^4.17.21",
Expand Down
4 changes: 2 additions & 2 deletions scripts/declarations/lint/index.mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const ESLINT_CONFIG_PATH = path.join(ROOT_PATH, '.eslintrc.yaml');
const eslint = new ESLint({ overrideConfigFile: ESLINT_CONFIG_PATH, fix: false });
const eslintWithFix = new ESLint({ overrideConfigFile: ESLINT_CONFIG_PATH, fix: true });

const declarationsPath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.services.declarationsPath'));
const instancePath = path.resolve(declarationsPath, '../');
const instancePath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.collectionPath'));
const declarationsPath = path.resolve(instancePath, services.DECLARATIONS_PATH);

export default async options => {
let servicesToValidate = options.services || [];
Expand Down
4 changes: 2 additions & 2 deletions scripts/declarations/validate/index.mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const fs = fsApi.promises;
const MIN_DOC_LENGTH = 100;
const SLOW_DOCUMENT_THRESHOLD = 10 * 1000; // number of milliseconds after which a document fetch is considered slow

const declarationsPath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.services.declarationsPath'));
const instancePath = path.resolve(declarationsPath, '../');
const instancePath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.collectionPath'));
const declarationsPath = path.resolve(instancePath, services.DECLARATIONS_PATH);

export default async options => {
const schemaOnly = options.schemaOnly || false;
Expand Down
6 changes: 3 additions & 3 deletions src/archivist/services/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fsApi from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { pathToFileURL } from 'url';

Expand All @@ -8,8 +8,8 @@ import Service from './service.js';
import SourceDocument from './sourceDocument.js';
import Terms from './terms.js';

const fs = fsApi.promises;
const declarationsPath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.services.declarationsPath'));
export const DECLARATIONS_PATH = './declarations';
const declarationsPath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.collectionPath'), DECLARATIONS_PATH);

export async function load(servicesIdsToLoad = []) {
let servicesIds = await getDeclaredServicesIds();
Expand Down
2 changes: 1 addition & 1 deletion src/archivist/services/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ export default class Service {
}

static getNumberOfTerms(services, servicesIds, termsTypes) {
return servicesIds.reduce((acc, serviceId) => acc + services[serviceId].getNumberOfTerms(termsTypes), 0);
return (servicesIds || Object.keys(services)).reduce((acc, serviceId) => acc + services[serviceId].getNumberOfTerms(termsTypes), 0);
}
}
14 changes: 12 additions & 2 deletions src/collection-api/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import path from 'path';

import config from 'config';
import express from 'express';
import helmet from 'helmet';

import * as Services from '../../archivist/services/index.js';

import docsRouter from './docs.js';
import metadataRouter from './metadata.js';
import servicesRouter from './services.js';
import versionsRouter from './versions.js';

export default function apiRouter(basePath) {
export default async function apiRouter(basePath) {
const router = express.Router();

const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives();
Expand All @@ -27,7 +33,11 @@ export default function apiRouter(basePath) {
res.json({ message: 'Welcome to an instance of the Open Terms Archive API. Documentation is available at /docs. Learn more on Open Terms Archive on https://opentermsarchive.org.' });
});

router.use(servicesRouter);
const collectionPath = path.resolve(process.cwd(), config.get('@opentermsarchive/engine.collectionPath'));
const services = await Services.load();

router.use(await metadataRouter(collectionPath, services));
router.use(servicesRouter(services));
router.use(versionsRouter);

return router;
Expand Down
165 changes: 165 additions & 0 deletions src/collection-api/routes/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import fs from 'fs/promises';
import path from 'path';

import express from 'express';
import yaml from 'js-yaml';

import Service from '../../archivist/services/service.js';

const METADATA_FILENAME = 'metadata.yml';
const PACKAGE_JSON_PATH = '../../../package.json';

/**
* @swagger
* tags:
* name: Metadata
* description: Collection metadata API
* components:
* schemas:
* Metadata:
* type: object
* description: Collection metadata
* properties:
* id:
* type: string
* description: Unique identifier of the collection
* name:
* type: string
* description: Display name of the collection
* tagline:
* type: string
* description: Short description of the collection
* description:
* type: string
* nullable: true
* description: Detailed description of the collection
* totalTerms:
* type: integer
* description: Total number of terms tracked in the collection
* totalServices:
* type: integer
* description: Total number of services tracked in the collection
* engineVersion:
* type: string
* description: Version of the Open Terms Archive engine in SemVer format (MAJOR.MINOR.PATCH)
* dataset:
* type: string
* format: uri
* description: URL to the dataset releases
* declarations:
* type: string
* format: uri
* description: URL to the declarations repository
* versions:
* type: string
* format: uri
* description: URL to the versions repository
* snapshots:
* type: string
* format: uri
* description: URL to the snapshots repository
* logo:
* type: string
* format: uri
* nullable: true
* description: URL to the collection logo
* languages:
* type: array
* items:
* type: string
* description: List of ISO 639 language codes representing languages allowed by the collection
* jurisdictions:
* type: array
* items:
* type: string
* description: List of ISO 3166-2 country codes representing jurisdictions covered by the collection
* trackingPeriods:
* type: array
* items:
* type: object
* properties:
* startDate:
* type: string
* format: date
* description: The date when tracking started for this period
* schedule:
* type: string
* description: A cron expression defining when terms are tracked (e.g. "0 0 * * *" for daily at midnight)
* serverLocation:
* type: string
* description: The geographic location of the server used for tracking
* endDate:
* type: string
* format: date
* description: The date when tracking ended for this period
* governance:
* type: object
* properties:
* hosts:
* type: array
* items:
* $ref: '#/components/schemas/Organization'
* administrators:
* type: array
* items:
* $ref: '#/components/schemas/Organization'
* curators:
* type: array
* items:
* $ref: '#/components/schemas/Organization'
* maintainers:
* type: array
* items:
* $ref: '#/components/schemas/Organization'
* sponsors:
* type: array
* items:
* $ref: '#/components/schemas/Organization'
* Organization:
* type: object
* properties:
* name:
* type: string
* description: Name of the organization
* url:
* type: string
* format: uri
* description: URL to the organization's website
* logo:
* type: string
* format: uri
* description: URL to the organization's logo
*/
export default async function metadataRouter(collectionPath, services) {
const router = express.Router();

const STATIC_METADATA = yaml.load(await fs.readFile(path.join(collectionPath, METADATA_FILENAME), 'utf8'));
const { version: engineVersion } = JSON.parse(await fs.readFile(new URL(PACKAGE_JSON_PATH, import.meta.url)));

/**
* @swagger
* /metadata:
* get:
* summary: Get collection metadata
* tags: [Metadata]
* produces:
* - application/json
* responses:
* 200:
* description: Collection metadata
*/
router.get('/metadata', (req, res) => {
const dynamicMetadata = {
totalServices: Object.keys(services).length,
totalTerms: Service.getNumberOfTerms(services),
engineVersion,
};

res.json({
...STATIC_METADATA,
...dynamicMetadata,
});
});

return router;
}
89 changes: 89 additions & 0 deletions src/collection-api/routes/metadata.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from 'fs/promises';

import { expect } from 'chai';
import config from 'config';
import request from 'supertest';

import app from '../server.js';

const basePath = config.get('@opentermsarchive/engine.collection-api.basePath');
const { version: engineVersion } = JSON.parse(await fs.readFile(new URL('../../../package.json', import.meta.url)));

const EXPECTED_RESPONSE = {
totalServices: 7,
totalTerms: 8,
id: 'test',
name: 'test',
tagline: 'Test collection',
description: 'This is a test collection used for testing purposes.',
dataset: 'https://github.com/OpenTermsArchive/test-versions/releases',
declarations: 'https://github.com/OpenTermsArchive/test-declarations',
versions: 'https://github.com/OpenTermsArchive/test-versions',
snapshots: 'https://github.com/OpenTermsArchive/test-snapshots',
donations: null,
logo: 'https://opentermsarchive.org/images/logo/logo-open-terms-archive-black.png',
languages: [
'en',
],
jurisdictions: [
'EU',
],
governance: {
hosts: [
{ name: 'Localhost' },
],
administrators: [
{
name: 'Open Terms Archive',
url: 'https://opentermsarchive.org/',
logo: 'https://opentermsarchive.org/images/logo/logo-open-terms-archive-black.png',
},
],
curators: [
{
name: 'Open Terms Archive',
url: 'https://opentermsarchive.org/',
logo: 'https://opentermsarchive.org/images/logo/logo-open-terms-archive-black.png',
},
],
maintainers: [
{
name: 'Open Terms Archive',
url: 'https://opentermsarchive.org/',
logo: 'https://opentermsarchive.org/images/logo/logo-open-terms-archive-black.png',
},
],
sponsors: [
{
name: 'Open Terms Archive',
url: 'https://opentermsarchive.org/',
logo: 'https://opentermsarchive.org/images/logo/logo-open-terms-archive-black.png',
},
],
},
};

describe('Metadata API', () => {
describe('GET /metadata', () => {
let response;

before(async () => {
response = await request(app).get(`${basePath}/v1/metadata`);
});

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 expected metadata object', () => {
expect(response.body).to.deep.equal({
...EXPECTED_RESPONSE,
engineVersion,
});
});
});
});
Loading
Loading