@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 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.2",
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
@@ -4,3 +4,4 @@
4
4
  export { Pipeline } from "./pipeline.js"
5
5
  export { ingest } from "./pipeline/ingest.js"
6
6
  export { federate } from "./pipeline/federate.js"
7
+ export { validate } from "./validate.js"
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
- ingest() { return ingest(this.root) }
11
- federate() { return federate(this.root) }
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
@@ -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
+ })