@_linked/server 3.8.0 → 3.9.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 CHANGED
@@ -1,5 +1,59 @@
1
1
  # @\_linked/server
2
2
 
3
+ ## 3.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#206](https://github.com/linked-fw/server/pull/206) [`dc25a49`](https://github.com/linked-fw/server/commit/dc25a490a9a115e366d99f8db644d236043d609e) Thanks [@flyon](https://github.com/flyon)! - The host control channel reads its secret from `APP_ADMIN_SECRET` and its requests carry it in the
8
+ `x-app-admin-secret` header. New route `GET /admin/routes` answers the graph routes the process
9
+ runs with: `{datasets: [{store, id?, default, routes, defaultGraphShapes}]}`, one entry per dataset
10
+ the router knows, `routes` as `{<shape IRI>: <graph IRI>}` as core's routing holds them once the
11
+ app's storage file has run, and the shapes each dataset writes to its default graph. Like every
12
+ `/admin/*` route it answers 404 when the process has no admin secret.
13
+
14
+ Renamed, with a fallback (each old name logs one deprecation warning per process):
15
+
16
+ - `CN_APP_ADMIN_SECRET` → `APP_ADMIN_SECRET`; the old variable is read when the new one is unset.
17
+ - The `x-cn-admin-secret` header → `x-app-admin-secret`; the old header is read when the new one
18
+ is absent.
19
+
20
+ The secret is compared through sha256 digests of both sides, so the comparison does not reveal
21
+ its length.
22
+
23
+ No breaking changes: an app or host that sets only the old names keeps working. Requires
24
+ `@_linked/core` `^2.39.1` (`getShapeGraphs` and `graphRoutingFor` in `utils/graphRouting`).
25
+
26
+ - [#206](https://github.com/linked-fw/server/pull/206) [`fe012b1`](https://github.com/linked-fw/server/commit/fe012b1291d45763d779e8a8b0e9c18900efbca4) Thanks [@flyon](https://github.com/flyon)! - Shape graphs in the app. The boot and `POST /admin/sync-shapes` sync the registered shapes with
27
+ core's `syncShapeGraphs`: each package's shapes in its own named graph
28
+ (`{package IRI}/graph/shapes`, a `ShapesGraph` record in the graph registry), skipped when the
29
+ stored content hash matches. An installed package's graph comes from its built
30
+ `lib/esm/data/shapes.ttl`; the app's own packages (the app and its workspace packages) are
31
+ generated in process, in dev through Vite's loader; the app's single-owner shapes go in
32
+ `{DATA_ROOT}/graph/shapes`. The boot logs the graphs it writes, and the route reports them as
33
+ `shapeGraphs`. A graph refusal `graph-not-routed`, and any graph refusal code the server does not
34
+ know, is answered 400.
35
+
36
+ Breaking changes:
37
+
38
+ - The default-graph shape sync is replaced: shapes are written into shape graphs, and the shapes an
39
+ earlier server wrote into the default graph are removed there once their package's graph holds
40
+ them. A store without graph queries still gets the default-graph sync.
41
+ - An installed package without `lib/esm/data/shapes.ttl` gets no shape graph, and its shapes are
42
+ not generated: packages need a release built with a cli that writes `shapes.ttl`.
43
+ - `ShapeSyncResult.written` (the route's `written`) counts the shape graphs written, not shapes;
44
+ `shapeGraphs` holds the detail.
45
+ - `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` drops the shape graphs of packages no longer registered
46
+ (never the app's own graph), instead of default-graph node shapes; it is skipped, with a warning,
47
+ while a registered package has no `shapes.ttl`.
48
+ - Requires `@_linked/core` `^2.39.1` (`shapes/syncShapeGraphs`, `shapes/shapesToTurtle`).
49
+
50
+ Deprecated: `materializeShapes` and `SyncShapesFn` in `utils/syncShapes` stay as they were, for
51
+ code built against an earlier server; `syncShapeGraphsInto` replaces them.
52
+
53
+ An own package whose shapes could not be generated (the dev loader failed and the server's core
54
+ holds none of them) is listed in `withoutTurtle`, with a warning, instead of being left out of the
55
+ report.
56
+
3
57
  ## 3.8.0
4
58
 
5
59
  ### Minor Changes
@@ -0,0 +1,26 @@
1
+ @prefix core: <https://linked.cm/ont/core/> .
2
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
3
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
4
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
6
+
7
+ <https://linked.cm/shape/server/LincdAPI>
8
+ a sh:NodeShape ;
9
+ sh:targetClass <https://linked.cm/ont/server/LincdAPI> .
10
+
11
+ <https://linked.cm/shape/server/LincdWebApp>
12
+ a sh:NodeShape ;
13
+ sh:property <https://linked.cm/shape/server/LincdWebApp/api> ;
14
+ sh:targetClass <https://linked.cm/ont/server/LincdWebApp> .
15
+
16
+ <https://linked.cm/shape/server/LincdWebApp/api>
17
+ a sh:PropertyShape ;
18
+ rdfs:label "api" ;
19
+ sh:maxCount "1"^^xsd:integer ;
20
+ sh:node <https://linked.cm/shape/server-utils/Lincd_API_Client> ;
21
+ sh:nodeKind sh:IRI ;
22
+ sh:path <https://linked.cm/ont/server/hasAPI> .
23
+
24
+ <https://linked.cm/shape/server/LinkedServer>
25
+ a sh:NodeShape ;
26
+ sh:targetClass <https://linked.cm/ont/server/LincdServer> .
@@ -84,6 +84,8 @@ export declare class LinkedServer extends Shape {
84
84
  private linkedPackageDirs;
85
85
  /** The app's own workspace packages (`linkedPackage: true`), by real path. */
86
86
  private workspacePackageDirs;
87
+ /** The names of the app's own workspace packages. */
88
+ private workspacePackageNames;
87
89
  private warnedDuplicateInstalls;
88
90
  private resizePathsMap;
89
91
  private api;
@@ -105,30 +107,32 @@ export declare class LinkedServer extends Shape {
105
107
  initOnly(): Promise<this>;
106
108
  private initOnlyBoot;
107
109
  /**
108
- * Materialize this server's registered shapes into the RDF store as SHACL
109
- * (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
110
- * app's app-data holds the shapes its instances validate/query against.
111
- * Whatever shapes the app package registers (published re-exports +
112
- * generated) are written on boot, and again when the host asks for it over
113
- * the control channel ({@link resyncShapes}). A source reload (HMR) does not
114
- * sync shapes by itself.
110
+ * Sync this server's registered shapes into app data, one named graph per package (see
111
+ * utils/syncShapes): each package's shapes in `{package IRI}/graph/shapes`, from its built
112
+ * `shapes.ttl` (an installed package without one gets no shape graph), the app's own packages
113
+ * generated in process in dev (through Vite's loader), and the app's generated single-owner
114
+ * shapes in `{DATA_ROOT}/graph/shapes`. Runs at boot, after the ontology sync, and again when
115
+ * the host asks for it over the control channel ({@link resyncShapes}). A source reload (HMR)
116
+ * does not sync shapes by itself.
115
117
  *
116
- * Core writes only the shapes whose stored hash differs, so a sync of
117
- * unchanged shapes writes nothing.
118
+ * A graph whose stored content hash matches is not written, so a sync of unchanged shapes
119
+ * writes nothing. Shapes an earlier sync wrote into the default graph are removed there once
120
+ * their package's graph holds them; a store without graph queries gets that default-graph sync
121
+ * instead.
118
122
  *
119
- * Additive only: store shapes this process did not register are left alone
120
- * unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
121
- * `bootOrphanScope` in utils/syncShapes for why.
123
+ * Shape graphs of packages no longer registered stay unless the operator sets
124
+ * `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see `bootOrphanScope` in utils/syncShapes.
122
125
  *
123
126
  * Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
124
- * LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
125
- * `syncShapesOnBoot: false` in its own linked.config — its default dataset is
126
- * a context-router that can't materialize context-free, and it syncs its own
127
- * pinned shapes separately (linked.backend.storage.js).
127
+ * LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. A host whose default
128
+ * dataset is a context router (one that can't write context-free) sets
129
+ * `syncShapesOnBoot: false` and syncs its shapes itself.
128
130
  */
129
131
  private materializeShapesIntoStore;
130
132
  /** One shape sync into the default dataset; throws when it fails. */
131
133
  private syncRegisteredShapes;
134
+ /** The root directory of a package with shapes: the app, a discovered linked package, or one the app resolves. */
135
+ private shapePackageDir;
132
136
  /**
133
137
  * Boot: sync the registered ontologies into their graphs in app data and run the shape
134
138
  * check, before the shape sync (see utils/ontologySync). The sync follows the shape sync's
@@ -137,9 +137,9 @@ import { resolveStaticAccessURL, staticAssetURL, } from '../utils/releaseManifes
137
137
  import { resolveBootstrapEntry, resolveViteMainEntry, resolveViteServingMode, } from '../utils/bootstrapEntry.js';
138
138
  import { resolveRouteAssets } from '../utils/routeAssets.js';
139
139
  import { resolveRouteComponent, ssrCssInlineUrls, ssrCssScope, } from '../utils/ssrCss.js';
140
- import { bootOrphanScope, materializeShapes } from '../utils/syncShapes.js';
140
+ import { appShapesGraph, bootOrphanScope, loadShapeGraphCore, syncShapeGraphsInto, viteShapesTurtleGenerator, } from '../utils/syncShapes.js';
141
141
  import { bootSyncsOntologies, loadOntologyCore, runOntologyPhase, ontologyPackageDirs, sourceModuleImporter, shapeSyncOn, syncOntologiesThenShapes, } from '../utils/ontologySync.js';
142
- import { AdminRequestError, assertReloadablePackages, registerAdminRoutes, } from '../utils/adminChannel.js';
142
+ import { AdminRequestError, assertReloadablePackages, registerAdminRoutes, resolveAdminSecret, } from '../utils/adminChannel.js';
143
143
  import { envMs, withTimeout } from '../utils/withTimeout.js';
144
144
  import { invalidateSourceUnder } from '../utils/reloadSource.js';
145
145
  import { installSpaFallback, repinSpaFallback, } from '../utils/spaFallback.js';
@@ -286,6 +286,8 @@ let LinkedServer = class LinkedServer extends Shape {
286
286
  this.linkedPackageDirs = new Map();
287
287
  /** The app's own workspace packages (`linkedPackage: true`), by real path. */
288
288
  this.workspacePackageDirs = [];
289
+ /** The names of the app's own workspace packages. */
290
+ this.workspacePackageNames = new Set();
289
291
  this.warnedDuplicateInstalls = new Set();
290
292
  //from resizedFileName to full resized path (CDN or similar)
291
293
  this.resizePathsMap = new Map();
@@ -366,49 +368,73 @@ let LinkedServer = class LinkedServer extends Shape {
366
368
  return this;
367
369
  }
368
370
  /**
369
- * Materialize this server's registered shapes into the RDF store as SHACL
370
- * (core `syncShapes` → pure `sh:NodeShape` + property shapes), so a running
371
- * app's app-data holds the shapes its instances validate/query against.
372
- * Whatever shapes the app package registers (published re-exports +
373
- * generated) are written on boot, and again when the host asks for it over
374
- * the control channel ({@link resyncShapes}). A source reload (HMR) does not
375
- * sync shapes by itself.
371
+ * Sync this server's registered shapes into app data, one named graph per package (see
372
+ * utils/syncShapes): each package's shapes in `{package IRI}/graph/shapes`, from its built
373
+ * `shapes.ttl` (an installed package without one gets no shape graph), the app's own packages
374
+ * generated in process in dev (through Vite's loader), and the app's generated single-owner
375
+ * shapes in `{DATA_ROOT}/graph/shapes`. Runs at boot, after the ontology sync, and again when
376
+ * the host asks for it over the control channel ({@link resyncShapes}). A source reload (HMR)
377
+ * does not sync shapes by itself.
376
378
  *
377
- * Core writes only the shapes whose stored hash differs, so a sync of
378
- * unchanged shapes writes nothing.
379
+ * A graph whose stored content hash matches is not written, so a sync of unchanged shapes
380
+ * writes nothing. Shapes an earlier sync wrote into the default graph are removed there once
381
+ * their package's graph holds them; a store without graph queries gets that default-graph sync
382
+ * instead.
379
383
  *
380
- * Additive only: store shapes this process did not register are left alone
381
- * unless the operator sets `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see
382
- * `bootOrphanScope` in utils/syncShapes for why.
384
+ * Shape graphs of packages no longer registered stay unless the operator sets
385
+ * `LINKED_SYNC_SHAPES_PRUNE_ORPHANS=true` — see `bootOrphanScope` in utils/syncShapes.
383
386
  *
384
387
  * Opt-out via `linked.config` `syncShapesOnBoot` (default true); env
385
- * LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. CN sets
386
- * `syncShapesOnBoot: false` in its own linked.config — its default dataset is
387
- * a context-router that can't materialize context-free, and it syncs its own
388
- * pinned shapes separately (linked.backend.storage.js).
388
+ * LINKED_SYNC_SHAPES_ON_BOOT overrides for deployment. A host whose default
389
+ * dataset is a context router (one that can't write context-free) sets
390
+ * `syncShapesOnBoot: false` and syncs its shapes itself.
389
391
  */
390
392
  async materializeShapesIntoStore() {
391
393
  try {
392
- await this.syncRegisteredShapes();
394
+ await runAsSystem(() => this.syncRegisteredShapes(), 'shape sync at boot');
393
395
  }
394
396
  catch (err) {
395
- console.warn('[LinkedServer] shape materialization failed (non-fatal):', err);
397
+ console.warn('[LinkedServer] shape sync failed (non-fatal):', err);
396
398
  }
397
399
  }
398
400
  /** One shape sync into the default dataset; throws when it fails. */
399
401
  async syncRegisteredShapes() {
400
- var _a;
402
+ var _a, _b, _c, _d, _e;
401
403
  if (!shapeSyncOn(process.env, (_a = this.config) === null || _a === void 0 ? void 0 : _a.syncShapesOnBoot)) {
402
404
  return { written: 0, skipped: 'disabled' };
403
405
  }
404
406
  const appData = LinkedStorage.getDefaultDataset();
405
407
  if (!appData)
406
408
  return { written: 0, skipped: 'no-default-dataset' };
407
- const { syncShapes } = await import('@_linked/core');
408
- // Explicit target: materialize EVERY registered shape into the app's own
409
- // data store regardless of per-shape routing/pins.
410
- const { count, orphanScope } = await materializeShapes(appData, syncShapes);
411
- return { written: count, orphanScope };
409
+ const orphanScope = bootOrphanScope();
410
+ const appName = (_b = this.package) === null || _b === void 0 ? void 0 : _b.name;
411
+ const vite = (_c = this.config.server) === null || _c === void 0 ? void 0 : _c.vite;
412
+ const generate = viteShapesTurtleGenerator(vite);
413
+ // Explicit target: every registered shape goes into the app's own data store, regardless of
414
+ // per-shape routing or pins.
415
+ const shapeGraphs = await syncShapeGraphsInto(appData, {
416
+ core: await loadShapeGraphCore(),
417
+ orphanScope,
418
+ isOwn: (name) => name === appName || this.workspacePackageNames.has(name),
419
+ dev: !!generate,
420
+ generate,
421
+ packageDirOf: (name) => this.shapePackageDir(name),
422
+ appGraph: appShapesGraph(),
423
+ appPackage: appName,
424
+ });
425
+ const written = (_d = shapeGraphs.defaultGraph) !== null && _d !== void 0 ? _d : shapeGraphs.written.length + (((_e = shapeGraphs.appGraph) === null || _e === void 0 ? void 0 : _e.status) === 'written' ? 1 : 0);
426
+ return { written, orphanScope, shapeGraphs };
427
+ }
428
+ /** The root directory of a package with shapes: the app, a discovered linked package, or one the app resolves. */
429
+ shapePackageDir(name) {
430
+ var _a, _b;
431
+ if (name === ((_a = this.package) === null || _a === void 0 ? void 0 : _a.name))
432
+ return process.cwd();
433
+ const known = (_b = this.linkedPackageDirs) === null || _b === void 0 ? void 0 : _b.get(name);
434
+ if (known)
435
+ return known;
436
+ const installed = readInstalledPackage(name, process.cwd());
437
+ return installed === null || installed === void 0 ? void 0 : installed.root;
412
438
  }
413
439
  /**
414
440
  * Boot: sync the registered ontologies into their graphs in app data and run the shape
@@ -726,8 +752,8 @@ let LinkedServer = class LinkedServer extends Shape {
726
752
  res.sendStatus(200);
727
753
  });
728
754
  // The host's control channel (/admin/*), open only when the host started
729
- // this process with CN_APP_ADMIN_SECRET; see utils/adminChannel.
730
- registerAdminRoutes(this.server, process.env.CN_APP_ADMIN_SECRET, {
755
+ // this process with APP_ADMIN_SECRET; see utils/adminChannel.
756
+ registerAdminRoutes(this.server, resolveAdminSecret(), {
731
757
  // The host's provisioner starts the app again after it exits.
732
758
  exit: () => process.exit(0),
733
759
  syncShapes: (request) => this.resyncShapes(request),
@@ -829,6 +855,7 @@ let LinkedServer = class LinkedServer extends Shape {
829
855
  // Local workspace packages in this app's dependency tree. These are also
830
856
  // imported whole at boot (indexLincdPackage), not only their /backend.
831
857
  const allLocalPackages = getLincdPackages();
858
+ this.workspacePackageNames = new Set(allLocalPackages.map((pkg) => pkg.packageName));
832
859
  this.workspacePackageDirs = allLocalPackages.map((pkg) => {
833
860
  try {
834
861
  return fsNative.realpathSync(pkg.path);