@directory-builder/core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/cli.js +7 -0
- package/package.json +2 -1
- package/src/index.js +1 -0
- package/src/pipeline.js +7 -2
- package/src/validate/federation.shacl.ttl +26 -0
- package/src/validate.js +52 -0
- package/test/validate.test.js +14 -0
package/README.md
CHANGED
|
@@ -88,6 +88,11 @@ npx directory-builder webapp build --base /repo/ # production build → weba
|
|
|
88
88
|
`webapp/{content,exporters}/` into `webapp/dist/` next to the bundle —
|
|
89
89
|
`webapp/dist/` is the complete site, ready to publish as-is.
|
|
90
90
|
|
|
91
|
+
The two are independent: the dev server never needs a prior build — `webapp
|
|
92
|
+
build` exists only to produce the deployable. Both show whatever `data/` the
|
|
93
|
+
pipeline last produced, so run the pipeline first (and rebuild before
|
|
94
|
+
publishing, or `dist/` keeps the stale snapshot).
|
|
95
|
+
|
|
91
96
|
For webapp development in this repo:
|
|
92
97
|
|
|
93
98
|
```sh
|
package/bin/cli.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// directory-builder run the full pipeline (ingest + federate)
|
|
6
6
|
// directory-builder ingest fetch + lift only
|
|
7
7
|
// directory-builder federate clean → map → match → merge → resolve only
|
|
8
|
+
// directory-builder validate check the instance's config ↔ sources/ integrity
|
|
8
9
|
// directory-builder webapp dev server for the instance's webapp
|
|
9
10
|
// directory-builder webapp build [--base /x/] build the webapp → <instance>/webapp/dist/
|
|
10
11
|
|
|
11
12
|
import { webappBuild, webappDev } from "../src/webapp.js"
|
|
12
13
|
import { Pipeline } from "../src/pipeline.js"
|
|
14
|
+
import { validate } from "../src/validate.js"
|
|
13
15
|
|
|
14
16
|
const [cmd = "run", ...rest] = process.argv.slice(2)
|
|
15
17
|
const flag = (name) => {
|
|
@@ -22,6 +24,11 @@ const commands = {
|
|
|
22
24
|
run: () => pipeline.run(),
|
|
23
25
|
ingest: () => pipeline.ingest(),
|
|
24
26
|
federate: () => pipeline.federate(),
|
|
27
|
+
validate: async () => {
|
|
28
|
+
const problems = await validate()
|
|
29
|
+
if (problems.length) { console.error(problems.join("\n")); process.exit(1) }
|
|
30
|
+
console.log("instance valid")
|
|
31
|
+
},
|
|
25
32
|
webapp: () => {
|
|
26
33
|
if (rest[0] && rest[0] !== "build") {
|
|
27
34
|
console.error(`Unknown webapp subcommand "${rest[0]}" — expected "build" or nothing (dev server)`)
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directory-builder/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Use-case-agnostic engine for config-driven federation pipelines",
|
|
5
5
|
"author": "Civic Data Lab",
|
|
6
6
|
"repository": "github:foederierter-datenpool/directory-builder-core",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
10
11
|
"example": "cd example && node ../bin/cli.js",
|
|
11
12
|
"webapp": "vite webapp",
|
|
12
13
|
"webapp:build": "vite build webapp"
|
package/src/index.js
CHANGED
package/src/pipeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ingest } from "./pipeline/ingest.js"
|
|
2
2
|
import { federate } from "./pipeline/federate.js"
|
|
3
|
+
import { validate } from "./validate.js"
|
|
3
4
|
|
|
4
5
|
// Programmatic entry: hold the instance root once, run the engines against it.
|
|
5
6
|
// The CLI (bin/cli.js) is this same class with defaults — root = cwd.
|
|
@@ -7,8 +8,12 @@ export class Pipeline {
|
|
|
7
8
|
constructor({ root = process.cwd() } = {}) {
|
|
8
9
|
this.root = root
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
async validate() {
|
|
12
|
+
const problems = await validate(this.root)
|
|
13
|
+
if (problems.length) throw new Error(`invalid instance at ${this.root}:\n ${problems.join("\n ")}`)
|
|
14
|
+
}
|
|
15
|
+
async ingest() { await this.validate(); return ingest(this.root) }
|
|
16
|
+
async federate() { await this.validate(); return federate(this.root) }
|
|
12
17
|
async run() {
|
|
13
18
|
await this.ingest()
|
|
14
19
|
await this.federate()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@prefix : <https://civic-data.de/pipeline#> .
|
|
2
|
+
@prefix sh: <http://www.w3.org/ns/shacl#> .
|
|
3
|
+
|
|
4
|
+
# The engine's contract for a well-formed federation.ttl
|
|
5
|
+
# expressed as SHACL and enforced by ../validate.js
|
|
6
|
+
|
|
7
|
+
:federationShape a sh:NodeShape ;
|
|
8
|
+
sh:targetNode :federation ;
|
|
9
|
+
sh:property [
|
|
10
|
+
sh:path :hasSource ;
|
|
11
|
+
sh:minCount 1 ;
|
|
12
|
+
sh:nodeKind sh:IRI ;
|
|
13
|
+
sh:message "needs at least one :hasSource"
|
|
14
|
+
] .
|
|
15
|
+
|
|
16
|
+
:sourceShape a sh:NodeShape ;
|
|
17
|
+
sh:targetObjectsOf :hasSource ;
|
|
18
|
+
sh:property [
|
|
19
|
+
sh:path :format ;
|
|
20
|
+
sh:minCount 1 ;
|
|
21
|
+
sh:maxCount 1 ;
|
|
22
|
+
sh:nodeKind sh:IRI ;
|
|
23
|
+
sh:message "needs exactly one :format"
|
|
24
|
+
] .
|
|
25
|
+
|
|
26
|
+
# TODO: add the rest
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildValidator, turtleToDataset } from "@foerderfunke/sem-ops-utils"
|
|
2
|
+
import { CDP, objectsOf, parseTtl, PATHS, shrink, sourceName } from "./utils.js"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import fs from "fs"
|
|
5
|
+
|
|
6
|
+
// Instance integrity checks. Each check takes { abs, ttl, quads } (path
|
|
7
|
+
// resolver rooted at the instance, federation.ttl raw + parsed) and returns
|
|
8
|
+
// problem strings. validate() runs them all; empty result = valid. Runs
|
|
9
|
+
// automatically before the engines; `directory-builder validate` triggers it
|
|
10
|
+
// on its own.
|
|
11
|
+
|
|
12
|
+
const checks = [sourcesFoldersInSync, federationConformsToShape]
|
|
13
|
+
|
|
14
|
+
export async function validate(root = process.cwd()) {
|
|
15
|
+
const abs = (p) => path.join(root, p)
|
|
16
|
+
if (!fs.existsSync(abs(PATHS.federation))) return [`${PATHS.federation} missing`]
|
|
17
|
+
const ttl = fs.readFileSync(abs(PATHS.federation), "utf8")
|
|
18
|
+
const ctx = { abs, ttl, quads: parseTtl(ttl) }
|
|
19
|
+
return (await Promise.all(checks.map((check) => check(ctx)))).flat()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Every :hasSource in federation.ttl has its sources/<name>/ folder with
|
|
23
|
+
// fetch.js + clean.sparql - and no folder exists that the federation doesn't
|
|
24
|
+
// declare. Checks all declared sources, enabled or not: folder presence is a
|
|
25
|
+
// repo-layout contract.
|
|
26
|
+
function sourcesFoldersInSync({ abs, quads }) {
|
|
27
|
+
const declared = objectsOf(quads, `${CDP}hasSource`).map(sourceName)
|
|
28
|
+
const problems = []
|
|
29
|
+
for (const name of declared) {
|
|
30
|
+
for (const file of [PATHS.fetchScript(name), PATHS.cleanQuery(name)]) {
|
|
31
|
+
if (!fs.existsSync(abs(file))) problems.push(`${file} missing`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const folders = fs.existsSync(abs("sources"))
|
|
35
|
+
? fs.readdirSync(abs("sources"), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
36
|
+
: []
|
|
37
|
+
for (const name of folders) {
|
|
38
|
+
if (!declared.includes(name)) problems.push(`sources/${name}/ has no :hasSource declaration in ${PATHS.federation}`)
|
|
39
|
+
}
|
|
40
|
+
return problems
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// federation.ttl conforms to the engine's config contract, expressed as SHACL
|
|
44
|
+
// in federation.shacl.ttl next to this file - the shape ships with the
|
|
45
|
+
// package, instances never carry it.
|
|
46
|
+
const validator = buildValidator(fs.readFileSync(path.join(import.meta.dirname, "validate/federation.shacl.ttl"), "utf8"))
|
|
47
|
+
|
|
48
|
+
async function federationConformsToShape({ ttl }) {
|
|
49
|
+
const report = await validator.validate({ dataset: turtleToDataset(ttl) })
|
|
50
|
+
return report.results.map((r) =>
|
|
51
|
+
`${PATHS.federation}: ${shrink(r.focusNode.value, { "": CDP })} ${r.message.map((m) => m.value).join("; ")}`)
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { validate } from "@directory-builder/core"
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import { test } from "node:test"
|
|
4
|
+
import path from "path"
|
|
5
|
+
|
|
6
|
+
const INSTANCE_ROOT = path.join(import.meta.dirname, "../example")
|
|
7
|
+
|
|
8
|
+
// The example instance satisfies the contract validate() enforces: every
|
|
9
|
+
// :hasSource in federation.ttl has its sources/<name>/ folder with fetch.js
|
|
10
|
+
// + clean.sparql, no folder exists that the federation doesn't declare, and
|
|
11
|
+
// federation.ttl conforms to the engine's SHACL shape.
|
|
12
|
+
test("validate() finds no problems in the example instance", async () => {
|
|
13
|
+
assert.deepEqual(await validate(INSTANCE_ROOT), [])
|
|
14
|
+
})
|