@_linked/server 3.12.1 → 3.14.0
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/CHANGELOG.md +72 -0
- package/lib/esm/backend.d.ts +23 -5
- package/lib/esm/backend.js +116 -25
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/LincdAPI.d.ts +1 -1
- package/lib/esm/shapes/LincdAPI.js +43 -7
- package/lib/esm/shapes/LincdAPI.js.map +1 -1
- package/lib/esm/shapes/LinkedServer.d.ts +59 -39
- package/lib/esm/shapes/LinkedServer.js +158 -93
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +52 -20
- package/lib/esm/shapes/quadstores/RemoteDataset.js +134 -32
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +23 -51
- package/lib/esm/utils/adminChannel.js +50 -38
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/effectiveRoutes.d.ts +6 -0
- package/lib/esm/utils/effectiveRoutes.js +2 -1
- package/lib/esm/utils/effectiveRoutes.js.map +1 -1
- package/lib/esm/utils/forwardedTransactions.d.ts +46 -0
- package/lib/esm/utils/forwardedTransactions.js +155 -0
- package/lib/esm/utils/forwardedTransactions.js.map +1 -0
- package/lib/esm/utils/graphTargets.d.ts +42 -17
- package/lib/esm/utils/graphTargets.js +62 -69
- package/lib/esm/utils/graphTargets.js.map +1 -1
- package/lib/esm/utils/linkedDependencies.d.ts +53 -7
- package/lib/esm/utils/linkedDependencies.js +130 -12
- package/lib/esm/utils/linkedDependencies.js.map +1 -1
- package/lib/esm/utils/ontologySync.d.ts +55 -31
- package/lib/esm/utils/ontologySync.js +94 -42
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +13 -0
- package/lib/esm/utils/queryPlane.js +49 -2
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/reloadSource.js +1 -1
- package/lib/esm/utils/reloadSource.js.map +1 -1
- package/lib/esm/utils/serverTransactions.d.ts +22 -0
- package/lib/esm/utils/serverTransactions.js +81 -0
- package/lib/esm/utils/serverTransactions.js.map +1 -0
- package/lib/esm/utils/syncShapes.d.ts +123 -6
- package/lib/esm/utils/syncShapes.js +166 -35
- package/lib/esm/utils/syncShapes.js.map +1 -1
- package/package.json +4 -4
- package/readme.md +158 -38
package/readme.md
CHANGED
|
@@ -25,6 +25,7 @@ If you want to further adjust the functionality of LincdServer yourself, either
|
|
|
25
25
|
- [Without `url`](#without-url)
|
|
26
26
|
- [With `url`](#with-url)
|
|
27
27
|
- [`ping`](#ping)
|
|
28
|
+
- [Store transactions](#store-transactions)
|
|
28
29
|
- [Shape catalog per request](#shape-catalog-per-request)
|
|
29
30
|
- [API-only servers (`server.apiOnly`)](#api-only-servers-serverapionly)
|
|
30
31
|
- [Host control channel (`/admin/*`)](#host-control-channel-admin)
|
|
@@ -314,7 +315,7 @@ Export path `@_linked/server/shapes/quadstores/RemoteDataset` (named export `Rem
|
|
|
314
315
|
| Answered by | this app's own backend | a remote data server, e.g. a host's gateway |
|
|
315
316
|
| Sends through | `Server.call`'s package form (`/call/@_linked/server/<method>`); on a backend, `Server`'s local shortcut | `globalThis.fetch` to `{url}/call/@_linked/server/<method>`, never `Server` |
|
|
316
317
|
| Authenticated by | the user's session | `Authorization: Bearer <token>` |
|
|
317
|
-
| `init()` | resolves
|
|
318
|
+
| `init()` | asks the backend's `ping` for transactions only, resolves `undefined` (a failed ping leaves them off) | posts `ping`, throws a typed error when the server cannot be used |
|
|
318
319
|
| Typical config | `linked.frontend.datasets.json` | `linked.backend.datasets.json` |
|
|
319
320
|
|
|
320
321
|
```typescript
|
|
@@ -323,7 +324,7 @@ export interface RemoteDatasetConfig {
|
|
|
323
324
|
token?: string; // sent as Authorization: Bearer <token> when set and url is set
|
|
324
325
|
name?: string; // id = ${DATA_ROOT}/backend-api-store/<name>; does not affect routing
|
|
325
326
|
id?: string;
|
|
326
|
-
|
|
327
|
+
forwardUserToken?: boolean | string; // url form: send the signed-in user's access token (default true)
|
|
327
328
|
}
|
|
328
329
|
```
|
|
329
330
|
|
|
@@ -349,26 +350,42 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
|
|
|
349
350
|
```
|
|
350
351
|
|
|
351
352
|
- Each method posts `{"args": [<query.toJSON()>]}` to `{url}/call/@_linked/server/<method>`
|
|
352
|
-
(`
|
|
353
|
-
`
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
`
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
353
|
+
(`readQuery` for every read: a select, a count or an ask, told apart by the DSL-JSON `op`;
|
|
354
|
+
`updateQuery`, `createQuery`, `deleteQuery`; `graphQuery` for a graph query) with
|
|
355
|
+
`Content-Type` and `Accept` set to `application/json` and, when `token` is non-empty,
|
|
356
|
+
`Authorization: Bearer <token>`. It resolves the parsed body; `null` is a valid result. Nothing
|
|
357
|
+
is retried, so a write is never applied twice. The store declares `supports: ['ask']` (plus
|
|
358
|
+
`'transactions'`, below); a triples or terms read is refused without being sent, as the
|
|
359
|
+
server's `readQuery` answers neither.
|
|
360
|
+
- The server's `readQuery` checks a read under the access rule of its kind, read from the body
|
|
361
|
+
before it is decoded: a select and a count under `select`, an ask under `ask`. A triples or terms
|
|
362
|
+
read, a graph query and a write posted to it answer 400.
|
|
363
|
+
- **The user's token.** When the query is made while a signed-in user's HTTP request is being
|
|
364
|
+
answered (the call context is `http`), the post asks the hook on
|
|
365
|
+
`globalThis[Symbol.for('@_linked/auth:userTokenHeaders')]` (exported as
|
|
366
|
+
`USER_TOKEN_HEADERS_HOOK`; `@_linked/auth`'s backend installs it) with the request and
|
|
367
|
+
`{method: 'POST', url}` (the exact URL posted to). When it answers, the post also carries
|
|
368
|
+
`x-linked-user-token: <the user's Solid-OIDC access token>` (exported as `USER_TOKEN_HEADER`) and
|
|
369
|
+
`DPoP: <a proof for this one request>`, beside the bearer. auth answers for a session signed in
|
|
370
|
+
through an issuer that gave a DPoP-bound JWT access token (webid.email does), while the token has
|
|
371
|
+
more than 30 seconds left; other sessions, a system context (`runAsSystem`), an anonymous request
|
|
372
|
+
and code outside any call send neither. A hook that throws or answers something else sends
|
|
373
|
+
neither (one warning per process) and the query still goes. The token still decides which data
|
|
374
|
+
the store reaches; the data server verifies the user's token and proof (`@_linked/auth`'s
|
|
375
|
+
`verifySolidAccessToken`) and acts as that user. `forwardUserToken: false` (or the string
|
|
376
|
+
`"false"`) turns it off. No other headers are sent.
|
|
377
|
+
|
|
378
|
+
> **Warning: on by default.** Every query of a signed-in user's request sends that user's access
|
|
379
|
+
> token to the data server at `url`. Each proof is for one method and URL and the server cannot
|
|
380
|
+
> make new ones (the key stays with the app), but it learns who the user is. Point `url` only at
|
|
381
|
+
> a server you trust with your users' identities (for an app CN hosts, CN's gateway). To send
|
|
382
|
+
> none, set it off in the dataset's config:
|
|
368
383
|
>
|
|
369
384
|
> ```json
|
|
370
|
-
> "config": { "url": "${DATA_SERVER_URL:-}", "token": "${DATA_SERVER_TOKEN:-}", "
|
|
385
|
+
> "config": { "url": "${DATA_SERVER_URL:-}", "token": "${DATA_SERVER_TOKEN:-}", "forwardUserToken": false }
|
|
371
386
|
> ```
|
|
387
|
+
- A redirect is never followed (`redirect: 'manual'`): a 3xx answer is a `RemoteDatasetError`
|
|
388
|
+
with its status, so the bearer and the user's token go to `url` only.
|
|
372
389
|
- Trailing slashes are trimmed from `url`. A `url` that is present but empty (an unset
|
|
373
390
|
`${DATA_SERVER_URL:-}`) never falls back to the no-url form: `init()` and every method throw
|
|
374
391
|
`RemoteDatasetUnreachableError` with `url: ''`.
|
|
@@ -377,9 +394,9 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
|
|
|
377
394
|
`init()`, so a boot that must fail loudly awaits it itself before registering the store.
|
|
378
395
|
- `init()` resolves the ping body, which the store also keeps as the readonly `info`
|
|
379
396
|
(`RemoteDatasetPingInfo`, frozen; `{}` when the body is not a JSON object; `undefined` before
|
|
380
|
-
a successful ping and in the no-url form). The generic ping answers `{ok: true}`; a
|
|
381
|
-
add fields naming the dataset it serves (CN's adds `projectId`, `branchName`,
|
|
382
|
-
`forked`), so a boot can log where its data goes:
|
|
397
|
+
a successful ping and in the no-url form). The generic ping answers `{ok: true, supports}`; a
|
|
398
|
+
gateway may add fields naming the dataset it serves (CN's adds `projectId`, `branchName`,
|
|
399
|
+
`slot`, `dataset`, `forked`), so a boot can log where its data goes:
|
|
383
400
|
|
|
384
401
|
```ts
|
|
385
402
|
const info = await store.init();
|
|
@@ -403,9 +420,43 @@ semantics are under [Error semantics](#error-semantics). `token` is ignored.
|
|
|
403
420
|
### `ping`
|
|
404
421
|
|
|
405
422
|
`POST /call/@_linked/server/ping` (a `public` method of this package's generic provider) reads
|
|
406
|
-
nothing, changes nothing and answers `{ "ok": true
|
|
407
|
-
|
|
408
|
-
|
|
423
|
+
nothing, changes nothing and answers `{ "ok": true, "supports": [] }`, or `"supports":
|
|
424
|
+
["transactions"]` when the server holds store transactions (below). A plain linked server has no
|
|
425
|
+
token concept; a host that serves the wire with tokens checks the token itself and answers 401
|
|
426
|
+
for a refused one.
|
|
427
|
+
|
|
428
|
+
### Store transactions
|
|
429
|
+
|
|
430
|
+
Core's store transactions (`LinkedStorage.begin()`, `LinkedStorage.transaction(fn)`) go through a
|
|
431
|
+
`RemoteDataset` and this server to the stores behind it:
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
POST /call/@_linked/server/beginTransaction {"args": []} → "<txId>"
|
|
435
|
+
POST /call/@_linked/server/commit {"args": ["<txId>"]} → null
|
|
436
|
+
POST /call/@_linked/server/rollback {"args": ["<txId>"]} → null
|
|
437
|
+
POST /call/@_linked/server/<x>Query {"args": [<query.toJSON(), with "txId">]}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- **`RemoteDataset`** declares `'transactions'` in `supports` only after a `ping` that listed
|
|
441
|
+
them (both forms), so a server that does not hold them is never sent a txId; until then core
|
|
442
|
+
warns once per store and runs a transaction's queries directly. `beginTransaction()`,
|
|
443
|
+
`commit(txId)` and `rollback(txId)` post to the methods of the same names, never retried.
|
|
444
|
+
- **The server** holds transactions only when every dataset `LinkedStorage` routes to declares
|
|
445
|
+
`'transactions'` (`ping` says so). It answers a txId of its own and keeps a core `Transaction`
|
|
446
|
+
for it, which opens a transaction in each store a query of it routes to (a second store gets
|
|
447
|
+
its own, with core's warning). A query carrying the txId is checked against the store rules as
|
|
448
|
+
if it were sent alone; a refused or failed one rolls the transaction back in every store. An
|
|
449
|
+
idle transaction is rolled back after 30 seconds.
|
|
450
|
+
- **The caller.** A transaction needs a session (401 without one) and is bound to the session's
|
|
451
|
+
user account; a backend-to-backend call with no request behind it is `system`. Anyone else's
|
|
452
|
+
request with that txId is refused and leaves the transaction as it was.
|
|
453
|
+
- **Refusals** (`{error, code}`): 501 `transactions-unsupported`, 409 `transaction-unknown` (never
|
|
454
|
+
begun, ended, failed or expired; a `rollback` of one resolves), 403 `transaction-not-yours`. A
|
|
455
|
+
query with a txId sent to `/api/*`, and a graph query with one, are refused (400). Any refusal
|
|
456
|
+
of a request that names the caller's own transaction (a body that does not decode included)
|
|
457
|
+
rolls that transaction back. A store's own refusal is passed back as for any failed query.
|
|
458
|
+
- A forwarder of its own (a gateway) does the same: it lists `'transactions'` only when the store
|
|
459
|
+
it forwards to does, and never runs a query that carries a txId outside its transaction.
|
|
409
460
|
|
|
410
461
|
### Shape catalog per request
|
|
411
462
|
|
|
@@ -458,10 +509,8 @@ A host that starts the app process controls it over HTTP routes on the app's own
|
|
|
458
509
|
| `GET /admin/health` | 200 `{status: 'ok'}` once the server listens (the boot ontology sync, shape check and shape sync have finished by then) |
|
|
459
510
|
| `POST /admin/restart` | 202, then the process exits; the host starts it again |
|
|
460
511
|
| `POST /admin/graph-registry-changed` | body `{dataset}`: the host changed that dataset's named graphs; the process drops its cached graph registry and answers 200 `{invalidated: dataset}`; 400 without a dataset name |
|
|
461
|
-
| `GET /admin/routes` | 200 `{datasets: [{store, id?, default, routes, defaultGraphShapes}], graphTargets, graphTargetEnv}`: the effective graph routes and graph
|
|
462
|
-
| `POST /admin/
|
|
463
|
-
|
|
464
|
-
`POST /admin/sync-shapes` runs the ontology sync and the shape check (see [Ontology sync and the shape check](#ontology-sync-and-the-shape-check)), then re-syncs the registered shapes into the app's data exactly as the boot does (see [Shape graphs](#shape-graphs): only graphs whose stored content hash differs are written, with the boot's orphan policy and `syncShapesOnBoot` / `LINKED_SYNC_SHAPES_ON_BOOT` switches). `written` counts the shape graphs written, and `shapeGraphs` reports them. The optional body `{packages: [...]}` names packages whose source the host changed; each must be a valid npm package name and the app itself or one of its direct dependencies, never an `@_linked/*` package; the list only says what to reload, it never filters the syncs. In dev their modules are invalidated in Vite and their backends reloaded before the sync, so a shape file written a moment ago is registered; a built app reloads nothing. `reloaded` lists the packages actually reloaded and `shapes` the node shape IRIs registered afterwards, so the host can check that the shape it wrote is there. Syncs run one at a time, as the system; one that takes longer than `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120000) fails and releases the queue. A source reload by itself (HMR) never syncs shapes. A failed ontology sync does not stop the shape sync; the route then answers 500 with the ontology error.
|
|
512
|
+
| `GET /admin/routes` | 200 `{datasets: [{store, id?, default, routes, defaultGraphShapes}], graphTargets, graphTargetEnv, graphWriter}`: the effective graph routes, graph targets and graph writer (see below) |
|
|
513
|
+
| `POST /admin/graph-content` | body `{packages?, reload?}`: 200 `{graphTargets, packages: [{name, shapes?, ontology?}], appGraph?, withoutTurtle, unreadable, reloaded}`, what the syncs would write, writing nothing, after reloading the packages named in `reload` (see [Graph writer and graph content](#graph-writer-and-graph-content)); 400 `{error}` for a malformed body or a package it may not reload; 409 `{error}` when a graph target is not `package`; 500 `{error}` when it fails or times out |
|
|
465
514
|
|
|
466
515
|
`GET /admin/routes` reports the graph routes the process runs with, as core's routing holds them once the app's storage file (`linked.backend.storage`) has run. Routes are written only there (`LinkedStorage.setDatasetForShapes(dataset, shapes, {graph})`); the app never pushes them anywhere, and this route lets a host check what actually runs. One entry per dataset the router knows, the default dataset first, then in the order they were pinned:
|
|
467
516
|
|
|
@@ -470,7 +519,7 @@ A host that starts the app process controls it over HTTP routes on the app's own
|
|
|
470
519
|
- `routes`: `{<shape IRI>: <graph IRI>}` as registered. A sub-shape without a route of its own is written to its nearest routed super-shape's graph and is not listed.
|
|
471
520
|
- `defaultGraphShapes`: the registered shapes that resolve to this dataset and are written to its default graph (no route of their own, no routed super-shape), sorted.
|
|
472
521
|
|
|
473
|
-
`graphTargets` is the process's one graph-target setting, `{ontologyGraph, shapeGraph}` (`package`, `default` or a shared graph IRI each; see [Ontology and shape graphs](#ontology-and-shape-graphs)): where the boot syncs write, for every dataset. `graphTargetEnv` holds `LINKED_ONTOLOGY_GRAPH` / `LINKED_SHAPE_GRAPH` as the process has them (unset ones left out), so a host can tell where the setting came from.
|
|
522
|
+
`graphWriter` is `LINKED_GRAPH_WRITER` (`app` or `host`; see [Graph writer and graph content](#graph-writer-and-graph-content)). `graphTargets` is the process's one graph-target setting, `{ontologyGraph, shapeGraph}` (`package`, `default` or a shared graph IRI each; see [Ontology and shape graphs](#ontology-and-shape-graphs)): where the boot syncs write, for every dataset. `graphTargetEnv` holds `LINKED_ONTOLOGY_GRAPH` / `LINKED_SHAPE_GRAPH` as the process has them (unset ones left out), so a host can tell where the setting came from.
|
|
474
523
|
|
|
475
524
|
```json
|
|
476
525
|
{"datasets": [{"store": "FusekiStore", "default": true,
|
|
@@ -482,23 +531,67 @@ A host that starts the app process controls it over HTTP routes on the app's own
|
|
|
482
531
|
|
|
483
532
|
Side effects to expect:
|
|
484
533
|
|
|
485
|
-
- **A reload briefly disposes the package's providers.** Reloading a package (on HMR, or for `
|
|
486
|
-
- **A restart drops in-flight requests.** `POST /admin/restart` exits the process 50 ms after answering.
|
|
534
|
+
- **A reload briefly disposes the package's providers.** Reloading a package (on HMR, or for `graph-content`'s `reload`) runs `dispose()` on its providers and constructs new ones; a request to that package's routes or methods that arrives in between can fail. Reloads run one at a time; one that takes longer than `LINKED_SOURCE_RELOAD_TIMEOUT_MS` (default 60000) rejects and releases the queue.
|
|
535
|
+
- **A restart drops in-flight requests.** `POST /admin/restart` exits the process 50 ms after answering.
|
|
487
536
|
- The channel listens on the app's public port, like every other route; only the secret protects it.
|
|
488
537
|
|
|
538
|
+
### Graph writer and graph content
|
|
539
|
+
|
|
540
|
+
`LINKED_GRAPH_WRITER` says who writes the app's shape and ontology graphs: `app` (the default) or
|
|
541
|
+
`host`. Any other value stops the boot, naming the variable. Under `host` (a host such as Create
|
|
542
|
+
Now writes them) the app writes no shape or ontology graph: the boot registers the ontologies,
|
|
543
|
+
loads the class hierarchy and runs the shape check but syncs nothing (it logs one line saying the
|
|
544
|
+
host writes the graphs). Under `app` the boot syncs them (see [Ontology sync and the shape
|
|
545
|
+
check](#ontology-sync-and-the-shape-check) and [Shape graphs](#shape-graphs)) unless `linked.config`
|
|
546
|
+
sets `syncShapesOnBoot: false`.
|
|
547
|
+
|
|
548
|
+
The host reads the graphs from `POST /admin/graph-content`, body `{packages?: string[], reload?:
|
|
549
|
+
string[]}`. `reload` names packages whose source the host changed; each must be a valid npm
|
|
550
|
+
package name and the app itself or one of its direct dependencies (its `linked.unpublishedPackages`
|
|
551
|
+
among them), never an `@_linked/*` package. In dev their modules are invalidated in Vite and their
|
|
552
|
+
backends reloaded first, then the ontologies are registered again and the shape check runs, so a
|
|
553
|
+
shape file written a moment ago is in the answer; a built app reloads nothing. `reloaded` lists the
|
|
554
|
+
packages actually reloaded. `packages` limits the answer; without it every registered package with
|
|
555
|
+
shapes or ontology data is answered. It writes nothing. Requests run one at a time, as the system;
|
|
556
|
+
one that takes longer than `LINKED_SHAPE_SYNC_TIMEOUT_MS` (default 120000) fails with 500 and
|
|
557
|
+
releases the queue. It answers:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{"graphTargets": {"ontologyGraph": "package", "shapeGraph": "package"},
|
|
561
|
+
"packages": [{"name": "@acme/music",
|
|
562
|
+
"shapes": {"graph": "https://linked.cm/pkg/acme-music/graph/shapes", "mediaType": "text/turtle",
|
|
563
|
+
"text": "@prefix …", "hash": "…", "sources": ["data/shapes.ttl"]},
|
|
564
|
+
"ontology": {"graph": "https://linked.cm/pkg/acme-music/graph/ontology",
|
|
565
|
+
"mediaType": "application/n-triples", "text": "<…> <…> <…> .\n", "hash": "…",
|
|
566
|
+
"sources": ["data/music.ttl"]}}],
|
|
567
|
+
"appGraph": {"graph": "https://acme.example/data/graph/shapes", "mediaType": "text/turtle",
|
|
568
|
+
"text": "…", "hash": "…", "sources": []},
|
|
569
|
+
"withoutTurtle": [], "unreadable": [], "reloaded": []}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Each `text` and `hash` is exactly what the `package`-mode sync writes and records as the graph's
|
|
573
|
+
`core:contentHash` (a shape graph: its Turtle and `shapeGraphHash`; an ontology graph: the
|
|
574
|
+
N-Triples of the package's data files and `ontologyGraphHash` of their raw texts, so the host must
|
|
575
|
+
not hash the text itself), and `sources` the record's `dcterms:source`. A package's shape Turtle
|
|
576
|
+
comes from where the shape sync takes it: an installed package without `shapes.ttl` has no `shapes`
|
|
577
|
+
here either and is listed in `withoutTurtle`. `unreadable` lists packages whose ontology data could
|
|
578
|
+
not be read. `appGraph` is `null` when the app has no single-owner shapes (drop the graph) and left
|
|
579
|
+
out without `DATA_ROOT`. The route answers 409 unless both `LINKED_ONTOLOGY_GRAPH` and
|
|
580
|
+
`LINKED_SHAPE_GRAPH` are `package`.
|
|
581
|
+
|
|
489
582
|
### Ontology sync and the shape check
|
|
490
583
|
|
|
491
|
-
Before its shape sync
|
|
584
|
+
Before its shape sync at boot, the server:
|
|
492
585
|
|
|
493
586
|
1. **Registers the installed packages' ontologies** (core `registerInstalledOntologies`): it imports `lib/esm/ontologies/*.register.js` of every linked package it discovered in the app's dependency tree, so a package the app only deep-imports shape files from (`@_linked/schema/shapes/Person`) is synced and checked too, and of every workspace package of the app (`linkedPackage: true` under its `workspaces`), which no package may import (a Create Now clone's `packages/<slug>-ont`). In dev, a package without a built `lib/` registers from `src/ontologies/*.register.ts` through Vite's `ssrLoadModule`; a built app skips it. A register module that fails is logged and the rest go on.
|
|
494
|
-
2. **Syncs the ontologies** (core `
|
|
495
|
-
3. **Checks the shapes** against that hierarchy (core `checkShapeHierarchy`, with every package's `dependencies` and `peerDependencies`). A running app is never stopped by it: each finding is logged,
|
|
587
|
+
2. **Syncs the ontologies** (core `syncOntologiesRouted`): every registered package's ontology data is written into each dataset the storage routing needs it in (see [Which datasets the syncs write to](#which-datasets-the-syncs-write-to)), where `LINKED_ONTOLOGY_GRAPH` says (one graph per package by default; see [Ontology and shape graphs](#ontology-and-shape-graphs)), so the store's RDFS view infers the class hierarchy. The log line names a shared graph or the default graph when the sync wrote there. A package whose stored hash matches is skipped, so an unchanged sync writes nothing. The log line names the packages written and skipped (a package whose data cannot be read is skipped with core's warning). Sync nodes of packages no longer registered are pruned only with `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true`, the shape sync's orphan policy.
|
|
588
|
+
3. **Checks the shapes** against that hierarchy (core `checkShapeHierarchy`, with every package's `dependencies` and `peerDependencies`). A running app is never stopped by it: each finding is logged, with a count by severity.
|
|
496
589
|
|
|
497
|
-
The ontology sync
|
|
590
|
+
The ontology sync and the shape sync have one switch: both run when the app writes its own graphs (`LINKED_GRAPH_WRITER` unset or `app`; see [Graph writer and graph content](#graph-writer-and-graph-content)) and `linked.config` does not set `syncShapesOnBoot: false`. With the sync off the boot still registers the packages' ontologies and runs the shape check, and `POST /admin/graph-content` with `reload` registers and checks again. The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded at boot either way. A failed ontology sync at boot is logged and the boot goes on.
|
|
498
591
|
|
|
499
592
|
### Shape graphs
|
|
500
593
|
|
|
501
|
-
After the ontology sync and the shape check, at boot
|
|
594
|
+
After the ontology sync and the shape check, at boot, the server syncs the registered shapes with core's `syncShapeGraphsRouted`, into each dataset the storage routing needs them in (see [Which datasets the syncs write to](#which-datasets-the-syncs-write-to)). This section describes the default, `LINKED_SHAPE_GRAPH=package`; with a shared graph IRI every package's shapes and the app's generated ones go into that one graph (`graphs` lists it, `appGraph.graph` is it), and with `default` the shapes go into the default graph (`defaultGraph` counts the writes). See [Ontology and shape graphs](#ontology-and-shape-graphs).
|
|
502
595
|
|
|
503
596
|
- **One graph per package.** Each package's shapes go in `{package IRI}/graph/shapes`, with a `ShapesGraph` record in the graph registry (never a hierarchy source). A graph whose stored content hash matches is not written, so an unchanged sync writes nothing; a changed one is replaced whole.
|
|
504
597
|
- **Where the Turtle comes from.** An installed package's built `lib/esm/data/shapes.ttl`, which `linked build` writes. An installed package without that file gets no shape graph: nothing is generated for it (a package with no shapes has none). The app's own packages (the app and its workspace packages) are generated in process: in dev through Vite's `ssrLoadModule` (core's `packageShapesTurtle` on the core the source shapes registered on, the same bytes `linked build` writes); in a built app from their `lib/esm/data/shapes.ttl` when they have one.
|
|
@@ -512,7 +605,34 @@ The boot logs one line, for example:
|
|
|
512
605
|
[LinkedServer] shape graph sync: written @acme/shop; skipped @_linked/schema; app graph written; graphs written https://linked.cm/pkg/acme-shop/graph/shapes, https://acme.example/data/graph/shapes; no shapes.ttl (no shape graph): @acme/legacy
|
|
513
606
|
```
|
|
514
607
|
|
|
515
|
-
|
|
608
|
+
The sync runs under the switch of [Ontology sync and the shape check](#ontology-sync-and-the-shape-check): the app writes its own graphs and `syncShapesOnBoot` is not `false`. Its report, `{written, skipped, pruned, graphs, appGraph: {graph, status}, withoutTurtle, cleaned, target}` (plus `defaultGraph`, the default-graph sync's writes, under `LINKED_SHAPE_GRAPH=default` or on a store without graph queries), is what the log line shows. `target` is the process's `shapeGraph` setting (see [Ontology and shape graphs](#ontology-and-shape-graphs)); with `fallback` set the store had no graph queries and the shapes went into the default graph instead. In a shared graph the rebuild covers the packages with Turtle: a package without it is left out (and logged), where the package mode keeps its old graph.
|
|
609
|
+
|
|
610
|
+
### Which datasets the syncs write to
|
|
611
|
+
|
|
612
|
+
The boot syncs follow the app's storage routing (`linked.backend.storage`), through core's
|
|
613
|
+
`@_linked/core/shapes/syncTargets` (its README's "Which datasets the syncs write to" has the rule):
|
|
614
|
+
|
|
615
|
+
- a package's **shape graph** goes to every dataset one of its shapes routes to, and to the
|
|
616
|
+
dataset(s) the `sh:NodeShape` / `sh:PropertyShape` shapes route to;
|
|
617
|
+
- a package's **ontology graph** goes to every dataset one of its shapes routes to, wherever a
|
|
618
|
+
shape targeting one of its classes routes, and to the dataset(s) of the ontology term shapes
|
|
619
|
+
(`rdfs:Class`, `owl:Class`, `rdf:Property`, …), so the hierarchy is where the instances are;
|
|
620
|
+
- the app's **own shape graph** goes to every dataset a shape it constrains routes to (the shapes
|
|
621
|
+
declaring `contains` properties and their sub-shapes), and those of the shape shapes.
|
|
622
|
+
|
|
623
|
+
An app without pins writes everything into its default dataset. With pins the boot logs
|
|
624
|
+
one line naming the datasets per graph kind, for example:
|
|
625
|
+
|
|
626
|
+
```
|
|
627
|
+
[LinkedServer] The boot syncs follow the routing: shape graphs → FusekiStore "app" (every package), FusekiStore "music" (@acme/music); app shape graph → FusekiStore "app"; ontology graphs → FusekiStore "app" (every package), FusekiStore "music" (@acme/music, @_linked/schema).
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Each sync then logs one line per dataset (` in FusekiStore "music"`), and its report adds
|
|
631
|
+
`datasets` (each dataset's own report). When a pin moves, a package graph routing no longer sends
|
|
632
|
+
to the **default** dataset is dropped there (`prunedGraphs`, one log line each); one left in
|
|
633
|
+
another dataset stays and is reported (`leftovers`, one warning), since a store several apps share
|
|
634
|
+
may hold another app's copy. A dataset that cannot be read or written fails alone: the others are
|
|
635
|
+
synced, then the sync fails naming it (the boot logs a warning and goes on).
|
|
516
636
|
|
|
517
637
|
## Provider lifecycle and HMR
|
|
518
638
|
|