Initial commit

This commit is contained in:
Claire Lovisa
2023-09-21 10:31:34 +02:00
commit 3de5d8f184
14 changed files with 396 additions and 0 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false

17
.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
},
extends: [
'eslint:recommended',
],
env: {
node: true
},
rules: {
semi: 'warn',
"no-console": "off"
},
};

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*~
node_modules/
package-lock.json
.log/

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=false

10
.woodpecker/.latest.yml Normal file
View File

@@ -0,0 +1,10 @@
pipeline:
build:
image: plugins/docker
settings:
repo: ${CI_REPO}
tags: latest
secrets: [ docker_username, docker_password ]
when:
branch: master
event: push

10
.woodpecker/.release.yml Normal file
View File

@@ -0,0 +1,10 @@
pipeline:
build:
image: plugins/docker
settings:
repo: ${CI_REPO}
tags: "${CI_COMMIT_TAG##v}"
secrets: [ docker_username, docker_password ]
when:
event: tag
tag: v*

View File

@@ -0,0 +1,9 @@
pipeline:
build:
image: plugins/docker
settings:
repo: ${CI_REPO}
dry_run: true
when:
event:
- pull_request

3
Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM semtech/mu-javascript-template:1.6.0
LABEL maintainer="redpencil.io <info@repdencil.io>"

9
LICENSE.md Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright 2023 Vlaamse Overheid
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
<a name="readme-top"></a>
<br />
<div align="center">
<h1 align="center">sync-ovo-numbers-service</h1>
<p align="center">
Service that syncs OVO numbers of OP to match with those in Wegwijs.
<br />
<a href="https://github.com/lblod/sync-ovo-numbers-service/issues">Report Bug</a>
·
<a href="https://github.com/lblod/sync-ovo-numbers-service/pulls">Open PR</a>
</p>
</div>
## 📖 Description
This service maintains OP's OVO numbers in sync together with [Wegwijs](https://wegwijs.vlaanderen.be/#/organisations), which is the official source of OVO numbers.
It gets triggered when a KBO is updated in OP as well as on a regular basis via a cron job to ensure our dataset is in sync with theirs.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## ⏩ Quick setup
### 🐋 Docker-compose.yml
```yaml
sync-ovo-numbers:
image: lblod/sync-ovo-numbers-service
links:
- db:database
labels:
- "logging=true"
restart: always
logging: *default-logging
```
### 🗒️ Config
// TODO probably no delta config if we go for the endpoint option right ?
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## 🔑 Environment variables
| ENV | description | default | required |
|---|---|---|---|
| xxx | xxx | | X |
<p align="right">(<a href="#readme-top">back to top</a>)</p>

69
app.js Normal file
View File

@@ -0,0 +1,69 @@
import { app, errorHandler } from 'mu';
import bodyParser from 'body-parser';
import fetch from 'node-fetch';
import { Delta } from "./lib/delta";
import { isKbo, getIdentifiers, constructOvoStructure, updateOvoNumberAndUri } from "./lib/queries";
app.use(bodyParser.json({
type: function (req) {
return /^application\/json/.test(req.get('content-type'));
}
}));
// TODO update readme with env variables etc at the end
// TODO current issue: when the KBO get updated, it doesn't refresh in the frontend.
// Alternative: endpoint to call in the frontend directly, that way we can trigger a refresh on the OVO ?
app.post('/delta', async function (req, res, next) {
try {
const entries = new Delta(req.body).getInsertsFor('https://data.vlaanderen.be/ns/generiek#lokaleIdentificator');
if (!entries.length) {
console.log('Delta dit not contain potential KBOs, awaiting the next batch!');
return res.status(204).send();
}
for (let entry of entries) {
const isEntryKbo = await isKbo(entry)
if (isEntryKbo) {
const identifiers = await getIdentifiers(entry);
const response = await fetch(`https://api.wegwijs.vlaanderen.be/v1/search/organisations?q=kboNumber:${identifiers.kbo}&fields=ovoNumber,kboNumber)`);
const data = await response.json();
let wegwijsOvo = null;
if (data.length) {
// We got a match on the KBO, getting the associated OVO back
const wegwijsInfo = data[0]; // Wegwijs should only have only one entry per KBO
if (wegwijsInfo.ovoNumber)
wegwijsOvo = wegwijsInfo.ovoNumber;
}
if (wegwijsOvo != identifiers.ovo) {
let ovoStructuredIdUri = identifiers.ovoStructuredId;
if (!ovoStructuredIdUri) {
ovoStructuredIdUri = await constructOvoStructure(entry);
}
await updateOvoNumberAndUri(ovoStructuredIdUri, wegwijsOvo);
}
}
}
return res.status(200).send().end();
} catch (e) {
console.log(`Something unexpected went wrong while handling delta task!`);
console.error(e);
return next(e);
}
});
// TODO CRON JOB (+ endpoint to easily test it?)
// 1x per day. Query all admin units that have a KBO in our DB, with OVO numbers (optional)
// Then we compare with wegwijs, if possible via one big query to get all the codes at once,
// but if not possible multiple smaller ones
// Then we compare the KBO / OVO couples and as soon as we have a mismatch, we correct it
app.use(errorHandler);

28
lib/delta.js Normal file
View File

@@ -0,0 +1,28 @@
import { flatten } from 'lodash';
export class Delta {
constructor(delta) {
this.delta = delta;
}
get inserts() {
return flatten(this.delta.map(changeSet => changeSet.inserts));
}
get deletes() {
return flatten(this.delta.map(changeSet => changeSet.deletes));
}
getInsertsFor(predicate) {
return this.inserts
.filter(t => t.predicate.value === predicate)
.map(t => t.subject.value);
}
getDeletesFor(predicate) {
return this.deletes
.filter(t => t.predicate.value === predicate)
.map(t => t.subject.value);
}
}

160
lib/queries.js Normal file
View File

@@ -0,0 +1,160 @@
import { uuid, sparqlEscapeUri, sparqlEscapeString } from "mu";
import { querySudo as query, updateSudo as update } from '@lblod/mu-auth-sudo';
export async function isKbo(subject) {
const queryStr = `
ASK {
VALUES ?g {
<http://mu.semte.ch/graphs/administrative-unit>
<http://mu.semte.ch/graphs/worship-service>
}
GRAPH ?g {
${sparqlEscapeUri(subject)} a <https://data.vlaanderen.be/ns/generiek#GestructureerdeIdentificator> ;
<https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?kbo .
?id <https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ${sparqlEscapeUri(subject)} ;
<http://www.w3.org/2004/02/skos/core#notation> ?notation .
FILTER (?notation IN ("KBO nummer"@nl, "KBO nummer"))
}
}
`;
const result = await query(queryStr);
return result.boolean;
}
// Assumptions on the config : in the path, ?s is the subject of the delta and ?matchingUuid the id
export async function getIdentifiers(kboStructuredId) {
const queryStr = `
SELECT DISTINCT ?kbo ?ovo ?ovoStructuredId WHERE {
VALUES ?g {
<http://mu.semte.ch/graphs/administrative-unit>
<http://mu.semte.ch/graphs/worship-service>
}
GRAPH ?g {
${sparqlEscapeUri(kboStructuredId)} a <https://data.vlaanderen.be/ns/generiek#GestructureerdeIdentificator> ;
<https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?kbo .
?kboId <https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ${sparqlEscapeUri(kboStructuredId)} ;
<http://www.w3.org/2004/02/skos/core#notation> ?kboNotation .
?bestuurseeheid <http://www.w3.org/ns/adms#identifier> ?kboId .
FILTER (?kboNotation IN ("KBO nummer"@nl, "KBO nummer"))
OPTIONAL {
?bestuurseeheid <http://www.w3.org/ns/adms#identifier> ?ovoId .
?ovoId <https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ?ovoStructuredId ;
<http://www.w3.org/2004/02/skos/core#notation> ?ovoNotation .
OPTIONAL { ?ovoStructuredId <https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?ovo . }
FILTER (?ovoNotation IN ("OVO-nummer"@nl, "OVO-nummer"))
}
}
}
`;
const result = await query(queryStr);
if (result.results.bindings.length) {
const binding = result.results.bindings[0]; // We should only have one KBO/OVO couple
return {
kbo: binding.kbo.value,
ovo: binding.ovo?.value,
ovoStructuredId: binding.ovoStructuredId?.value,
}
}
return null;
}
export async function constructOvoStructure(kboStructuredId) {
const idUuid = uuid();
const idUri = `http://data.lblod.info/id/identificatoren/${idUuid}`
const structuredIdUuid = uuid();
const structuredIdUri = `http://data.lblod.info/id/gestructureerdeIdentificatoren/${structuredIdUuid}`
const updateStr = `
INSERT {
GRAPH ?g {
?bestuurseenheid <http://www.w3.org/ns/adms#identifier> ${sparqlEscapeUri(idUri)} .
${sparqlEscapeUri(idUri)} a <http://www.w3.org/ns/adms#Identifier> ;
<http://mu.semte.ch/vocabularies/core/uuid> ${sparqlEscapeString(idUuid)} ;
<http://www.w3.org/2004/02/skos/core#notation> "OVO-nummer" ;
<https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ${sparqlEscapeUri(structuredIdUri)} .
${sparqlEscapeUri(structuredIdUri)} a <https://data.vlaanderen.be/ns/generiek#GestructureerdeIdentificator> ;
<http://mu.semte.ch/vocabularies/core/uuid> ${sparqlEscapeString(structuredIdUuid)} .
}
} WHERE {
VALUES ?g {
<http://mu.semte.ch/graphs/administrative-unit>
<http://mu.semte.ch/graphs/worship-service>
}
GRAPH ?g {
${sparqlEscapeUri(kboStructuredId)} a <https://data.vlaanderen.be/ns/generiek#GestructureerdeIdentificator> ;
<https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?kbo .
?kboId <https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ${sparqlEscapeUri(kboStructuredId)} ;
<http://www.w3.org/2004/02/skos/core#notation> ?notation .
?bestuurseenheid <http://www.w3.org/ns/adms#identifier> ?kboId .
FILTER (?notation IN ("KBO nummer"@nl, "KBO nummer"))
}
}
`;
await update(updateStr);
return structuredIdUri;
}
export async function updateOvoNumberAndUri(ovoStructuredIdUri, wegwijsOvo) {
let updateStr = `
DELETE {
GRAPH ?g {
?bestuureseenheid <http://www.w3.org/2002/07/owl#sameAs> ?ovoUri .
${sparqlEscapeUri(ovoStructuredIdUri)} <https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?ovo .
}
}
`;
if (wegwijsOvo) {
updateStr += `
INSERT {
GRAPH ?g {
?bestuureseenheid <http://www.w3.org/2002/07/owl#sameAs> ${sparqlEscapeUri(`http://data.vlaanderen.be/id/organisatie/${wegwijsOvo}`)} .
${sparqlEscapeUri(ovoStructuredIdUri)} <https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ${sparqlEscapeString(wegwijsOvo)} .
}
}
`;
}
updateStr += `
WHERE {
VALUES ?g {
<http://mu.semte.ch/graphs/administrative-unit>
<http://mu.semte.ch/graphs/worship-service>
}
GRAPH ?g {
${sparqlEscapeUri(ovoStructuredIdUri)} a <https://data.vlaanderen.be/ns/generiek#GestructureerdeIdentificator> .
OPTIONAL {
?bestuureseenheid
<http://www.w3.org/ns/adms#identifier>/<https://data.vlaanderen.be/ns/generiek#gestructureerdeIdentificator> ${sparqlEscapeUri(ovoStructuredIdUri)} ;
<http://www.w3.org/2002/07/owl#sameAs> ?ovoUri .
}
OPTIONAL { ${sparqlEscapeUri(ovoStructuredIdUri)} <https://data.vlaanderen.be/ns/generiek#lokaleIdentificator> ?ovo . }
}
}
`;
await update(updateStr);
}
// Update queries: via mu-auth if it comes from the frontend ?

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "sync-ovo-numbers-service",
"version": "0.0.0",
"description": "Syncs OVO numbers with Wegwijs based on KBOs",
"author": "redpencil.io",
"license": "MIT",
"dependencies": {
"@lblod/mu-auth-sudo": "^0.6.1",
"lodash.flatten": "4.4.0",
"node-fetch": "^2.6.1"
}
}