Initial commit
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
17
.eslintrc.js
Normal 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
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*~
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.log/
|
||||
10
.woodpecker/.latest.yml
Normal file
10
.woodpecker/.latest.yml
Normal 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
10
.woodpecker/.release.yml
Normal 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*
|
||||
9
.woodpecker/.test-build.yml
Normal file
9
.woodpecker/.test-build.yml
Normal 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
3
Dockerfile
Normal 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
9
LICENSE.md
Normal 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
51
README.md
Normal 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
69
app.js
Normal 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
28
lib/delta.js
Normal 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
160
lib/queries.js
Normal 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
12
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user