@_linked/server 3.7.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 +99 -0
- package/lib/esm/backend.d.ts +9 -0
- package/lib/esm/backend.js +28 -1
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/data/shapes.ttl +26 -0
- package/lib/esm/shapes/LinkedServer.d.ts +23 -19
- package/lib/esm/shapes/LinkedServer.js +63 -33
- package/lib/esm/shapes/LinkedServer.js.map +1 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.d.ts +41 -1
- package/lib/esm/shapes/quadstores/RemoteDataset.js +152 -4
- package/lib/esm/shapes/quadstores/RemoteDataset.js.map +1 -1
- package/lib/esm/utils/adminChannel.d.ts +57 -4
- package/lib/esm/utils/adminChannel.js +125 -15
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/effectiveRoutes.d.ts +26 -0
- package/lib/esm/utils/effectiveRoutes.js +40 -0
- package/lib/esm/utils/effectiveRoutes.js.map +1 -0
- package/lib/esm/utils/ontologySync.d.ts +24 -6
- package/lib/esm/utils/ontologySync.js +30 -9
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +21 -0
- package/lib/esm/utils/queryPlane.js +60 -0
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/syncShapes.d.ts +136 -26
- package/lib/esm/utils/syncShapes.js +175 -26
- package/lib/esm/utils/syncShapes.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +40 -3
|
@@ -1,25 +1,86 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
var _b, _c;
|
|
1
3
|
/**
|
|
2
|
-
* The host→app control channel: `/admin/*` routes a host
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* a standalone app has no control channel. Every request carries the secret in
|
|
6
|
-
* the `x-
|
|
4
|
+
* The host→app control channel: `/admin/*` routes a host uses on an app process it
|
|
5
|
+
* started. Present only when the process was started with a shared secret in
|
|
6
|
+
* `APP_ADMIN_SECRET` (see {@link resolveAdminSecret}); without it every route answers
|
|
7
|
+
* 404, so a standalone app has no control channel. Every request carries the secret in
|
|
8
|
+
* the `x-app-admin-secret` header (401 otherwise).
|
|
7
9
|
*
|
|
8
10
|
* - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot
|
|
9
11
|
* shape sync has finished by then).
|
|
10
12
|
* - `POST /admin/restart` → 202, then the process exits; the host starts it again.
|
|
11
|
-
* - `POST /admin/sync-shapes` → re-syncs the app's ontologies (
|
|
12
|
-
*
|
|
13
|
-
* re-syncs its shapes into its app data
|
|
14
|
-
*
|
|
13
|
+
* - `POST /admin/sync-shapes` → re-syncs the app's ontologies (each registered package's
|
|
14
|
+
* ontology graph, skipped when unchanged), runs the shape check and
|
|
15
|
+
* re-syncs its shapes into its app data (each package's shape graph and the app's own,
|
|
16
|
+
* skipped when unchanged), as the boot does, and answers 200 with
|
|
17
|
+
* {@link ShapeSyncResult}, whose `shapeGraphs` reports the shape graphs. Body (optional):
|
|
15
18
|
* `{packages: string[]}`, packages whose source changed on disk: in dev they are
|
|
16
19
|
* reloaded first, so the sync sees shapes added or changed since the boot. Each
|
|
17
20
|
* name must be a valid npm package name and the app itself or one of its direct
|
|
18
21
|
* dependencies, never an `@_linked/*` framework package; anything else answers
|
|
19
22
|
* 400 `{error}`. The list never filters the syncs. A failed or timed-out sync
|
|
20
23
|
* answers 500 `{error}` (a failed ontology sync too, after the shape sync ran).
|
|
24
|
+
* - `POST /admin/graph-registry-changed` `{dataset}` → the host changed the named graphs of that
|
|
25
|
+
* dataset (a graph query through its gateway): the process drops its cached graph registry, so
|
|
26
|
+
* its next `.from(graphs.allOntologies)` read re-reads it, and answers 200
|
|
27
|
+
* `{invalidated: dataset}`. A body without a dataset name answers 400. Without the push the
|
|
28
|
+
* cache's 30-second expiry applies.
|
|
29
|
+
* - `GET /admin/routes` → 200 {@link EffectiveRoutes}: the graph routes of every dataset the
|
|
30
|
+
* router knows (shape IRI → graph IRI) and the shapes each one writes to its default graph,
|
|
31
|
+
* as core's routing holds them once the app's storage file has run. The routes are written
|
|
32
|
+
* only in the app's storage file; this route lets a host check what actually runs.
|
|
33
|
+
*
|
|
34
|
+
* Deprecated names, still read: `CN_APP_ADMIN_SECRET` when `APP_ADMIN_SECRET` is unset, and the
|
|
35
|
+
* `x-cn-admin-secret` header when `x-app-admin-secret` is absent; each logs one warning.
|
|
36
|
+
*/
|
|
37
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
38
|
+
import { invalidateGraphRegistry } from '@_linked/core/utils/graphRegistryCache';
|
|
39
|
+
import { effectiveGraphRoutes } from './effectiveRoutes.js';
|
|
40
|
+
/** The environment variable that holds the control channel's secret. */
|
|
41
|
+
export const ADMIN_SECRET_ENV = 'APP_ADMIN_SECRET';
|
|
42
|
+
/** Its former name, read when {@link ADMIN_SECRET_ENV} is unset. @deprecated */
|
|
43
|
+
export const LEGACY_ADMIN_SECRET_ENV = 'CN_APP_ADMIN_SECRET';
|
|
44
|
+
/** The request header that carries the secret. */
|
|
45
|
+
export const ADMIN_SECRET_HEADER = 'x-app-admin-secret';
|
|
46
|
+
/** Its former name, read when {@link ADMIN_SECRET_HEADER} is absent. @deprecated */
|
|
47
|
+
export const LEGACY_ADMIN_SECRET_HEADER = 'x-cn-admin-secret';
|
|
48
|
+
/**
|
|
49
|
+
* Which deprecation warnings this process has logged. On the shared global, so a module that
|
|
50
|
+
* evaluates twice still warns once.
|
|
21
51
|
*/
|
|
22
|
-
|
|
52
|
+
const warned = ((_a = (_b = globalThis)[_c = Symbol.for('@_linked/server:adminChannelDeprecationsWarned')]) !== null && _a !== void 0 ? _a : (_b[_c] = new Set()));
|
|
53
|
+
function warnOnce(key, message, warn) {
|
|
54
|
+
if (warned.has(key))
|
|
55
|
+
return;
|
|
56
|
+
warned.add(key);
|
|
57
|
+
warn(message);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The control channel's secret: `APP_ADMIN_SECRET`, else the deprecated `CN_APP_ADMIN_SECRET`
|
|
61
|
+
* (logged once per process), else `undefined` (no control channel).
|
|
62
|
+
*/
|
|
63
|
+
export function resolveAdminSecret(env = process.env, warn = console.warn) {
|
|
64
|
+
const secret = env[ADMIN_SECRET_ENV];
|
|
65
|
+
if (secret)
|
|
66
|
+
return secret;
|
|
67
|
+
const legacy = env[LEGACY_ADMIN_SECRET_ENV];
|
|
68
|
+
if (!legacy)
|
|
69
|
+
return undefined;
|
|
70
|
+
warnOnce('env', `[linked] ${LEGACY_ADMIN_SECRET_ENV} is deprecated; set ${ADMIN_SECRET_ENV} instead (read as a fallback for now).`, warn);
|
|
71
|
+
return legacy;
|
|
72
|
+
}
|
|
73
|
+
/** The secret a request carries: the `x-app-admin-secret` header, else the deprecated one. */
|
|
74
|
+
function providedSecret(req, warn) {
|
|
75
|
+
const current = req.get(ADMIN_SECRET_HEADER);
|
|
76
|
+
if (current)
|
|
77
|
+
return current;
|
|
78
|
+
const legacy = req.get(LEGACY_ADMIN_SECRET_HEADER);
|
|
79
|
+
if (legacy) {
|
|
80
|
+
warnOnce('header', `[linked] the ${LEGACY_ADMIN_SECRET_HEADER} header is deprecated; send ${ADMIN_SECRET_HEADER} instead.`, warn);
|
|
81
|
+
}
|
|
82
|
+
return legacy;
|
|
83
|
+
}
|
|
23
84
|
/** npm's package-name rule: optional `@scope/`, lower case, URL-safe, no leading `.` or `_` (a scope may start with `_`, as `@_linked` does). */
|
|
24
85
|
const NPM_NAME = /^(?:@[a-z0-9-~_][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
25
86
|
/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */
|
|
@@ -45,13 +106,35 @@ export function assertReloadablePackages(packages, allowed) {
|
|
|
45
106
|
/** Thrown by a handler for a request the channel refuses with 400. */
|
|
46
107
|
export class AdminRequestError extends Error {
|
|
47
108
|
}
|
|
48
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Drop the process's cached graph registry after the host changed the graphs of `dataset`.
|
|
111
|
+
*
|
|
112
|
+
* Every entry is dropped, not only that dataset's: an entry's key is each store's own
|
|
113
|
+
* (`graphRegistryKey()`: a `FusekiStore` keys by its dataset URL, a `RemoteDataset` by the
|
|
114
|
+
* dataset its server names), no store tells which key it uses, and the stores are built by the
|
|
115
|
+
* app's storage config, whose aliases the server does not see. The registry is small, so the cost
|
|
116
|
+
* is one registry read per dataset on its next expanding read.
|
|
117
|
+
*/
|
|
118
|
+
export function dropGraphRegistryCache(_dataset) {
|
|
119
|
+
invalidateGraphRegistry();
|
|
120
|
+
}
|
|
121
|
+
/** The `dataset` of a graph-registry-changed body: a non-empty name. */
|
|
122
|
+
export function parseGraphRegistryChangedBody(body) {
|
|
123
|
+
const dataset = body && typeof body === 'object' ? body.dataset : undefined;
|
|
124
|
+
if (typeof dataset !== 'string' || !dataset.trim() || dataset.length > 1024) {
|
|
125
|
+
throw new AdminRequestError('dataset must be the name of the dataset whose graphs changed');
|
|
126
|
+
}
|
|
127
|
+
return { dataset: dataset.trim() };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compare the provided secret with the configured one in constant time. Both sides are hashed
|
|
131
|
+
* first, so the comparison does not reveal the secret's length either.
|
|
132
|
+
*/
|
|
49
133
|
export function adminSecretMatches(secret, provided) {
|
|
50
134
|
if (!secret || !provided)
|
|
51
135
|
return false;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
136
|
+
const digest = (value) => createHash('sha256').update(value).digest();
|
|
137
|
+
return timingSafeEqual(digest(provided), digest(secret));
|
|
55
138
|
}
|
|
56
139
|
/** The `packages` of a sync-shapes body: a list of package names, or none. */
|
|
57
140
|
export function parseSyncShapesBody(body) {
|
|
@@ -75,11 +158,12 @@ export function parseSyncShapesBody(body) {
|
|
|
75
158
|
/** Mount the control channel's routes on `app`. */
|
|
76
159
|
export function registerAdminRoutes(app, secret, handlers) {
|
|
77
160
|
const requireAdmin = (req, res) => {
|
|
161
|
+
var _a;
|
|
78
162
|
if (!secret) {
|
|
79
163
|
res.sendStatus(404); // no control channel in this mode
|
|
80
164
|
return false;
|
|
81
165
|
}
|
|
82
|
-
if (!adminSecretMatches(secret, req.
|
|
166
|
+
if (!adminSecretMatches(secret, providedSecret(req, (_a = handlers.warn) !== null && _a !== void 0 ? _a : console.warn))) {
|
|
83
167
|
res.sendStatus(401);
|
|
84
168
|
return false;
|
|
85
169
|
}
|
|
@@ -96,6 +180,32 @@ export function registerAdminRoutes(app, secret, handlers) {
|
|
|
96
180
|
res.sendStatus(202);
|
|
97
181
|
setTimeout(handlers.exit, 50);
|
|
98
182
|
});
|
|
183
|
+
app.get('/admin/routes', (req, res) => {
|
|
184
|
+
var _a;
|
|
185
|
+
if (!requireAdmin(req, res))
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
res.status(200).json(((_a = handlers.routes) !== null && _a !== void 0 ? _a : effectiveGraphRoutes)());
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
app.post('/admin/graph-registry-changed', (req, res) => {
|
|
195
|
+
var _a;
|
|
196
|
+
if (!requireAdmin(req, res))
|
|
197
|
+
return;
|
|
198
|
+
let dataset;
|
|
199
|
+
try {
|
|
200
|
+
({ dataset } = parseGraphRegistryChangedBody(req.body));
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
res.status(400).json({ error: err.message });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
((_a = handlers.graphRegistryChanged) !== null && _a !== void 0 ? _a : dropGraphRegistryCache)(dataset);
|
|
207
|
+
res.status(200).json({ invalidated: dataset });
|
|
208
|
+
});
|
|
99
209
|
app.post('/admin/sync-shapes', async (req, res) => {
|
|
100
210
|
if (!requireAdmin(req, res))
|
|
101
211
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminChannel.js","sourceRoot":"","sources":["../../../src/utils/adminChannel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The host→app control channel: `/admin/*` routes a host (Create Now) uses on an\n * app process it started. Present only when the process was started with a\n * shared secret in `CN_APP_ADMIN_SECRET`; without it every route answers 404, so\n * a standalone app has no control channel. Every request carries the secret in\n * the `x-cn-admin-secret` header.\n *\n * - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot\n * shape sync has finished by then).\n * - `POST /admin/restart` → 202, then the process exits; the host starts it again.\n * - `POST /admin/sync-shapes` → re-syncs the app's ontologies (the class hierarchy\n * of every registered package, skipped when unchanged), runs the shape check and\n * re-syncs its shapes into its app data, as the boot does, and answers 200 with\n * {@link ShapeSyncResult}. Body (optional):\n * `{packages: string[]}`, packages whose source changed on disk: in dev they are\n * reloaded first, so the sync sees shapes added or changed since the boot. Each\n * name must be a valid npm package name and the app itself or one of its direct\n * dependencies, never an `@_linked/*` framework package; anything else answers\n * 400 `{error}`. The list never filters the syncs. A failed or timed-out sync\n * answers 500 `{error}` (a failed ontology sync too, after the shape sync ran).\n */\nimport { timingSafeEqual } from 'crypto';\nimport type express from 'express';\n\n/** What one shape sync did. */\nexport interface ShapeSyncResult {\n /** Shapes written: those whose stored hash differed from the registered shape. */\n written: number;\n /** The orphan policy the sync ran with (see utils/syncShapes). */\n orphanScope?: 'all' | 'none';\n /** Set when no sync ran: shape sync is turned off, or there is no default dataset. */\n skipped?: 'disabled' | 'no-default-dataset';\n /**\n * The requested packages whose source was reloaded before the sync (dev only: a built\n * app reloads nothing). Set by `POST /admin/sync-shapes`.\n */\n reloaded?: string[];\n /** The node shape IRIs registered in the process after the reload. Set by `POST /admin/sync-shapes`. */\n shapes?: string[];\n /**\n * The ontology sync: packages whose class hierarchy was written, and packages left as\n * they were (unchanged, or their data could not be read). Set by `POST /admin/sync-shapes`.\n */\n ontologies?: { written: string[]; skipped: string[] };\n /** The shape check's findings, counted by severity (each is logged). Set by `POST /admin/sync-shapes`. */\n shapeCheck?: { errors: number; warnings: number };\n}\n\n/** npm's package-name rule: optional `@scope/`, lower case, URL-safe, no leading `.` or `_` (a scope may start with `_`, as `@_linked` does). */\nconst NPM_NAME = /^(?:@[a-z0-9-~_][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\n\n/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */\nexport function isValidPackageName(name: string): boolean {\n return name.length <= 214 && NPM_NAME.test(name) && !name.split('/').some((part) => part === '.' || part === '..');\n}\n\n/**\n * Refuse a package a host may not ask to reload: not a valid npm name, a framework\n * (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct\n * dependencies). Throws {@link AdminRequestError}.\n */\nexport function assertReloadablePackages(packages: string[], allowed: ReadonlySet<string>): void {\n for (const pkg of packages) {\n if (!isValidPackageName(pkg)) throw new AdminRequestError(`not a package name: ${JSON.stringify(pkg)}`);\n if (pkg.startsWith('@_linked/')) throw new AdminRequestError(`framework packages are not reloaded: ${pkg}`);\n if (!allowed.has(pkg)) {\n throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies`);\n }\n }\n}\n\n/** Thrown by a handler for a request the channel refuses with 400. */\nexport class AdminRequestError extends Error {}\n\nexport interface AdminChannelHandlers {\n /** Exit the process (the host restarts it). Called after the 202 is sent. */\n exit: () => void;\n /** Reload `packages` (when given) and sync the registered shapes into app data. */\n syncShapes: (request: { packages: string[] }) => Promise<ShapeSyncResult>;\n}\n\n/** Compare the provided secret with the configured one in constant time. */\nexport function adminSecretMatches(\n secret: string | undefined,\n provided: string | undefined\n): boolean {\n if (!secret || !provided) return false;\n const a = Buffer.from(provided);\n const b = Buffer.from(secret);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\n/** The `packages` of a sync-shapes body: a list of package names, or none. */\nexport function parseSyncShapesBody(body: unknown): { packages: string[] } {\n if (body === undefined || body === null || body === '') return { packages: [] };\n if (typeof body !== 'object') {\n throw new AdminRequestError('the body must be a JSON object');\n }\n const packages = (body as { packages?: unknown }).packages;\n if (packages === undefined) return { packages: [] };\n if (!Array.isArray(packages) || packages.some((p) => typeof p !== 'string' || !p)) {\n throw new AdminRequestError('packages must be a list of package names');\n }\n for (const p of packages as string[]) {\n if (!isValidPackageName(p)) throw new AdminRequestError(`not a package name: ${JSON.stringify(p)}`);\n }\n return { packages: [...new Set(packages as string[])] };\n}\n\n/** Mount the control channel's routes on `app`. */\nexport function registerAdminRoutes(\n app: express.Express,\n secret: string | undefined,\n handlers: AdminChannelHandlers\n): void {\n const requireAdmin = (req: express.Request, res: express.Response): boolean => {\n if (!secret) {\n res.sendStatus(404); // no control channel in this mode\n return false;\n }\n if (!adminSecretMatches(secret, req.get('x-cn-admin-secret'))) {\n res.sendStatus(401);\n return false;\n }\n return true;\n };\n\n app.get('/admin/health', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.status(200).json({ status: 'ok' });\n });\n\n app.post('/admin/restart', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.sendStatus(202);\n setTimeout(handlers.exit, 50);\n });\n\n app.post('/admin/sync-shapes', async (req, res) => {\n if (!requireAdmin(req, res)) return;\n let request: { packages: string[] };\n try {\n request = parseSyncShapesBody(req.body);\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n try {\n res.status(200).json(await handlers.syncShapes(request));\n } catch (err) {\n const status = err instanceof AdminRequestError ? 400 : 500;\n res.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"adminChannel.js","sourceRoot":"","sources":["../../../src/utils/adminChannel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The host→app control channel: `/admin/*` routes a host uses on an app process it\n * started. Present only when the process was started with a shared secret in\n * `APP_ADMIN_SECRET` (see {@link resolveAdminSecret}); without it every route answers\n * 404, so a standalone app has no control channel. Every request carries the secret in\n * the `x-app-admin-secret` header (401 otherwise).\n *\n * - `GET /admin/health` → 200 `{status: 'ok'}` once the server listens (the boot\n * shape sync has finished by then).\n * - `POST /admin/restart` → 202, then the process exits; the host starts it again.\n * - `POST /admin/sync-shapes` → re-syncs the app's ontologies (each registered package's\n * ontology graph, skipped when unchanged), runs the shape check and\n * re-syncs its shapes into its app data (each package's shape graph and the app's own,\n * skipped when unchanged), as the boot does, and answers 200 with\n * {@link ShapeSyncResult}, whose `shapeGraphs` reports the shape graphs. Body (optional):\n * `{packages: string[]}`, packages whose source changed on disk: in dev they are\n * reloaded first, so the sync sees shapes added or changed since the boot. Each\n * name must be a valid npm package name and the app itself or one of its direct\n * dependencies, never an `@_linked/*` framework package; anything else answers\n * 400 `{error}`. The list never filters the syncs. A failed or timed-out sync\n * answers 500 `{error}` (a failed ontology sync too, after the shape sync ran).\n * - `POST /admin/graph-registry-changed` `{dataset}` → the host changed the named graphs of that\n * dataset (a graph query through its gateway): the process drops its cached graph registry, so\n * its next `.from(graphs.allOntologies)` read re-reads it, and answers 200\n * `{invalidated: dataset}`. A body without a dataset name answers 400. Without the push the\n * cache's 30-second expiry applies.\n * - `GET /admin/routes` → 200 {@link EffectiveRoutes}: the graph routes of every dataset the\n * router knows (shape IRI → graph IRI) and the shapes each one writes to its default graph,\n * as core's routing holds them once the app's storage file has run. The routes are written\n * only in the app's storage file; this route lets a host check what actually runs.\n *\n * Deprecated names, still read: `CN_APP_ADMIN_SECRET` when `APP_ADMIN_SECRET` is unset, and the\n * `x-cn-admin-secret` header when `x-app-admin-secret` is absent; each logs one warning.\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport type express from 'express';\nimport { invalidateGraphRegistry } from '@_linked/core/utils/graphRegistryCache';\nimport type { ShapeGraphsReport } from './syncShapes.js';\nimport { effectiveGraphRoutes, type EffectiveRoutes } from './effectiveRoutes.js';\n\n/** The environment variable that holds the control channel's secret. */\nexport const ADMIN_SECRET_ENV = 'APP_ADMIN_SECRET';\n/** Its former name, read when {@link ADMIN_SECRET_ENV} is unset. @deprecated */\nexport const LEGACY_ADMIN_SECRET_ENV = 'CN_APP_ADMIN_SECRET';\n/** The request header that carries the secret. */\nexport const ADMIN_SECRET_HEADER = 'x-app-admin-secret';\n/** Its former name, read when {@link ADMIN_SECRET_HEADER} is absent. @deprecated */\nexport const LEGACY_ADMIN_SECRET_HEADER = 'x-cn-admin-secret';\n\n/**\n * Which deprecation warnings this process has logged. On the shared global, so a module that\n * evaluates twice still warns once.\n */\nconst warned: Set<string> = ((globalThis as Record<symbol, unknown>)[\n Symbol.for('@_linked/server:adminChannelDeprecationsWarned')\n] ??= new Set<string>()) as Set<string>;\n\nfunction warnOnce(key: string, message: string, warn: (message: string) => void): void {\n if (warned.has(key)) return;\n warned.add(key);\n warn(message);\n}\n\n/**\n * The control channel's secret: `APP_ADMIN_SECRET`, else the deprecated `CN_APP_ADMIN_SECRET`\n * (logged once per process), else `undefined` (no control channel).\n */\nexport function resolveAdminSecret(\n env: Record<string, string | undefined> = process.env,\n warn: (message: string) => void = console.warn\n): string | undefined {\n const secret = env[ADMIN_SECRET_ENV];\n if (secret) return secret;\n const legacy = env[LEGACY_ADMIN_SECRET_ENV];\n if (!legacy) return undefined;\n warnOnce(\n 'env',\n `[linked] ${LEGACY_ADMIN_SECRET_ENV} is deprecated; set ${ADMIN_SECRET_ENV} instead (read as a fallback for now).`,\n warn\n );\n return legacy;\n}\n\n/** The secret a request carries: the `x-app-admin-secret` header, else the deprecated one. */\nfunction providedSecret(req: express.Request, warn: (message: string) => void): string | undefined {\n const current = req.get(ADMIN_SECRET_HEADER);\n if (current) return current;\n const legacy = req.get(LEGACY_ADMIN_SECRET_HEADER);\n if (legacy) {\n warnOnce(\n 'header',\n `[linked] the ${LEGACY_ADMIN_SECRET_HEADER} header is deprecated; send ${ADMIN_SECRET_HEADER} instead.`,\n warn\n );\n }\n return legacy;\n}\n\n/** What one shape sync did. */\nexport interface ShapeSyncResult {\n /**\n * Shape graphs written (package graphs and the app's own graph) because their stored hash\n * differed; on a store without graph queries, the default-graph sync's writes.\n */\n written: number;\n /** The orphan policy the sync ran with (see utils/syncShapes). */\n orphanScope?: 'all' | 'none';\n /** Set when no sync ran: shape sync is turned off, or there is no default dataset. */\n skipped?: 'disabled' | 'no-default-dataset';\n /**\n * The shape graph sync: packages written and skipped, the graphs written, the app's own shape\n * graph, the registered packages without `shapes.ttl` (so without a shape graph) and the\n * default-graph shapes removed. Absent when no sync ran.\n */\n shapeGraphs?: ShapeGraphsReport;\n /**\n * The requested packages whose source was reloaded before the sync (dev only: a built\n * app reloads nothing). Set by `POST /admin/sync-shapes`.\n */\n reloaded?: string[];\n /** The node shape IRIs registered in the process after the reload. Set by `POST /admin/sync-shapes`. */\n shapes?: string[];\n /**\n * The ontology sync: packages whose ontology graph (or, on a store without graph queries,\n * class hierarchy) was written, packages left as they were (unchanged, or their data could not\n * be read), and the graphs written and dropped. Set by `POST /admin/sync-shapes`.\n */\n ontologies?: { written: string[]; skipped: string[]; graphs?: string[]; droppedGraphs?: string[] };\n /** The shape check's findings, counted by severity (each is logged). Set by `POST /admin/sync-shapes`. */\n shapeCheck?: { errors: number; warnings: number };\n}\n\n/** npm's package-name rule: optional `@scope/`, lower case, URL-safe, no leading `.` or `_` (a scope may start with `_`, as `@_linked` does). */\nconst NPM_NAME = /^(?:@[a-z0-9-~_][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\n\n/** Whether `name` is a valid npm package name (and so can never be a path like `..`). */\nexport function isValidPackageName(name: string): boolean {\n return name.length <= 214 && NPM_NAME.test(name) && !name.split('/').some((part) => part === '.' || part === '..');\n}\n\n/**\n * Refuse a package a host may not ask to reload: not a valid npm name, a framework\n * (`@_linked/*`) package, or not in `allowed` (the app's own name and its direct\n * dependencies). Throws {@link AdminRequestError}.\n */\nexport function assertReloadablePackages(packages: string[], allowed: ReadonlySet<string>): void {\n for (const pkg of packages) {\n if (!isValidPackageName(pkg)) throw new AdminRequestError(`not a package name: ${JSON.stringify(pkg)}`);\n if (pkg.startsWith('@_linked/')) throw new AdminRequestError(`framework packages are not reloaded: ${pkg}`);\n if (!allowed.has(pkg)) {\n throw new AdminRequestError(`unknown package: ${pkg} is neither the app nor one of its direct dependencies`);\n }\n }\n}\n\n/** Thrown by a handler for a request the channel refuses with 400. */\nexport class AdminRequestError extends Error {}\n\nexport interface AdminChannelHandlers {\n /** Exit the process (the host restarts it). Called after the 202 is sent. */\n exit: () => void;\n /** Reload `packages` (when given) and sync the registered shapes into app data. */\n syncShapes: (request: { packages: string[] }) => Promise<ShapeSyncResult>;\n /**\n * Drop the cached graph registry for `dataset` (see `POST /admin/graph-registry-changed`).\n * Default: {@link dropGraphRegistryCache}.\n */\n graphRegistryChanged?: (dataset: string) => void;\n /** The effective graph routes (see `GET /admin/routes`). Default: {@link effectiveGraphRoutes}. */\n routes?: () => EffectiveRoutes;\n /** Where deprecation warnings go. Default: `console.warn`. */\n warn?: (message: string) => void;\n}\n\n/**\n * Drop the process's cached graph registry after the host changed the graphs of `dataset`.\n *\n * Every entry is dropped, not only that dataset's: an entry's key is each store's own\n * (`graphRegistryKey()`: a `FusekiStore` keys by its dataset URL, a `RemoteDataset` by the\n * dataset its server names), no store tells which key it uses, and the stores are built by the\n * app's storage config, whose aliases the server does not see. The registry is small, so the cost\n * is one registry read per dataset on its next expanding read.\n */\nexport function dropGraphRegistryCache(_dataset: string): void {\n invalidateGraphRegistry();\n}\n\n/** The `dataset` of a graph-registry-changed body: a non-empty name. */\nexport function parseGraphRegistryChangedBody(body: unknown): { dataset: string } {\n const dataset = body && typeof body === 'object' ? (body as { dataset?: unknown }).dataset : undefined;\n if (typeof dataset !== 'string' || !dataset.trim() || dataset.length > 1024) {\n throw new AdminRequestError('dataset must be the name of the dataset whose graphs changed');\n }\n return { dataset: dataset.trim() };\n}\n\n/**\n * Compare the provided secret with the configured one in constant time. Both sides are hashed\n * first, so the comparison does not reveal the secret's length either.\n */\nexport function adminSecretMatches(\n secret: string | undefined,\n provided: string | undefined\n): boolean {\n if (!secret || !provided) return false;\n const digest = (value: string) => createHash('sha256').update(value).digest();\n return timingSafeEqual(digest(provided), digest(secret));\n}\n\n/** The `packages` of a sync-shapes body: a list of package names, or none. */\nexport function parseSyncShapesBody(body: unknown): { packages: string[] } {\n if (body === undefined || body === null || body === '') return { packages: [] };\n if (typeof body !== 'object') {\n throw new AdminRequestError('the body must be a JSON object');\n }\n const packages = (body as { packages?: unknown }).packages;\n if (packages === undefined) return { packages: [] };\n if (!Array.isArray(packages) || packages.some((p) => typeof p !== 'string' || !p)) {\n throw new AdminRequestError('packages must be a list of package names');\n }\n for (const p of packages as string[]) {\n if (!isValidPackageName(p)) throw new AdminRequestError(`not a package name: ${JSON.stringify(p)}`);\n }\n return { packages: [...new Set(packages as string[])] };\n}\n\n/** Mount the control channel's routes on `app`. */\nexport function registerAdminRoutes(\n app: express.Express,\n secret: string | undefined,\n handlers: AdminChannelHandlers\n): void {\n const requireAdmin = (req: express.Request, res: express.Response): boolean => {\n if (!secret) {\n res.sendStatus(404); // no control channel in this mode\n return false;\n }\n if (!adminSecretMatches(secret, providedSecret(req, handlers.warn ?? console.warn))) {\n res.sendStatus(401);\n return false;\n }\n return true;\n };\n\n app.get('/admin/health', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.status(200).json({ status: 'ok' });\n });\n\n app.post('/admin/restart', (req, res) => {\n if (!requireAdmin(req, res)) return;\n res.sendStatus(202);\n setTimeout(handlers.exit, 50);\n });\n\n app.get('/admin/routes', (req, res) => {\n if (!requireAdmin(req, res)) return;\n try {\n res.status(200).json((handlers.routes ?? effectiveGraphRoutes)());\n } catch (err) {\n res.status(500).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n\n app.post('/admin/graph-registry-changed', (req, res) => {\n if (!requireAdmin(req, res)) return;\n let dataset: string;\n try {\n ({ dataset } = parseGraphRegistryChangedBody(req.body));\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n (handlers.graphRegistryChanged ?? dropGraphRegistryCache)(dataset);\n res.status(200).json({ invalidated: dataset });\n });\n\n app.post('/admin/sync-shapes', async (req, res) => {\n if (!requireAdmin(req, res)) return;\n let request: { packages: string[] };\n try {\n request = parseSyncShapesBody(req.body);\n } catch (err) {\n res.status(400).json({ error: (err as Error).message });\n return;\n }\n try {\n res.status(200).json(await handlers.syncShapes(request));\n } catch (err) {\n const status = err instanceof AdminRequestError ? 400 : 500;\n res.status(status).json({ error: err instanceof Error ? err.message : String(err) });\n }\n });\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** One dataset the router knows, and its graph routes. */
|
|
2
|
+
export interface DatasetRoutes {
|
|
3
|
+
/**
|
|
4
|
+
* The store's class name (`FusekiStore`, `RemoteDataset`, …). The server never sees the
|
|
5
|
+
* aliases of the storage config, so a dataset is told apart by this, `default` and `id`.
|
|
6
|
+
*/
|
|
7
|
+
store: string;
|
|
8
|
+
/** The store's `id`, when it has one (a named `RemoteDataset` does). */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Whether this is the default dataset (the one for every shape without a pin). */
|
|
11
|
+
default: boolean;
|
|
12
|
+
/** The dataset's routes as registered: shape IRI → graph IRI. */
|
|
13
|
+
routes: Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* The registered shapes that resolve to this dataset and whose instances are written to its
|
|
16
|
+
* default graph: no route of their own and no routed super-shape. Sorted.
|
|
17
|
+
*/
|
|
18
|
+
defaultGraphShapes: string[];
|
|
19
|
+
}
|
|
20
|
+
/** The body of `GET /admin/routes`. */
|
|
21
|
+
export interface EffectiveRoutes {
|
|
22
|
+
/** Every dataset the router knows, the default one first, then in the order they were pinned. */
|
|
23
|
+
datasets: DatasetRoutes[];
|
|
24
|
+
}
|
|
25
|
+
/** The routes of every dataset the router knows (see {@link EffectiveRoutes}). */
|
|
26
|
+
export declare function effectiveGraphRoutes(): EffectiveRoutes;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The graph routes the process runs with, as core's routing holds them once the app's storage
|
|
3
|
+
* file has run (`LinkedStorage.setDatasetForShapes(dataset, shapes, {graph})`, or `loadStores`
|
|
4
|
+
* for a datasets config that still carries routes). Answered by `GET /admin/routes`, so a host
|
|
5
|
+
* can compare what it wrote into the storage file with what actually runs.
|
|
6
|
+
*/
|
|
7
|
+
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
8
|
+
import { getShapeGraphs, graphRoutingFor } from '@_linked/core/utils/graphRouting';
|
|
9
|
+
import { getAllShapeClasses } from '@_linked/core/utils/ShapeClass';
|
|
10
|
+
import { graphs } from '@_linked/core/ontologies/graphs';
|
|
11
|
+
/** The routes of every dataset the router knows (see {@link EffectiveRoutes}). */
|
|
12
|
+
export function effectiveGraphRoutes() {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
const datasets = [...LinkedStorage.getDatasets()];
|
|
15
|
+
const defaultDataset = LinkedStorage.getDefaultDataset();
|
|
16
|
+
const defaultGraphShapes = new Map(datasets.map((ds) => [ds, []]));
|
|
17
|
+
for (const [iri, shapeClass] of getAllShapeClasses()) {
|
|
18
|
+
const dataset = LinkedStorage.getDatasetForShapeClass(shapeClass);
|
|
19
|
+
const list = dataset && defaultGraphShapes.get(dataset);
|
|
20
|
+
if (!list)
|
|
21
|
+
continue;
|
|
22
|
+
const graph = (_b = (_a = graphRoutingFor(dataset)) === null || _a === void 0 ? void 0 : _a.graphForShape(iri)) !== null && _b !== void 0 ? _b : graphs.default;
|
|
23
|
+
if (graph === graphs.default)
|
|
24
|
+
list.push(iri);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
datasets: datasets.map((dataset) => {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const id = dataset.id;
|
|
30
|
+
return {
|
|
31
|
+
store: ((_a = dataset === null || dataset === void 0 ? void 0 : dataset.constructor) === null || _a === void 0 ? void 0 : _a.name) || 'unknown',
|
|
32
|
+
...(typeof id === 'string' && id ? { id } : {}),
|
|
33
|
+
default: dataset === defaultDataset,
|
|
34
|
+
routes: getShapeGraphs(dataset),
|
|
35
|
+
defaultGraphShapes: ((_b = defaultGraphShapes.get(dataset)) !== null && _b !== void 0 ? _b : []).sort(),
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=effectiveRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectiveRoutes.js","sourceRoot":"","sources":["../../../src/utils/effectiveRoutes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAC;AA6BzD,kFAAkF;AAClF,MAAM,UAAU,oBAAoB;;IAClC,MAAM,QAAQ,GAAG,CAAC,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC,CAAC;IAClD,MAAM,cAAc,GAAG,aAAa,CAAC,iBAAiB,EAAE,CAAC;IACzD,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAqB,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACvF,KAAK,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,kBAAkB,EAAE,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;QAClE,MAAM,IAAI,GAAG,OAAO,IAAI,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,KAAK,GAAG,MAAA,MAAA,eAAe,CAAC,OAAO,CAAC,0CAAE,aAAa,CAAC,GAAG,CAAC,mCAAI,MAAM,CAAC,OAAO,CAAC;QAC7E,IAAI,KAAK,KAAK,MAAM,CAAC,OAAO;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;;YACjC,MAAM,EAAE,GAAI,OAA4B,CAAC,EAAE,CAAC;YAC5C,OAAO;gBACL,KAAK,EAAE,CAAA,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,WAAW,0CAAE,IAAI,KAAI,SAAS;gBAC9C,GAAG,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,OAAO,EAAE,OAAO,KAAK,cAAc;gBACnC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC;gBAC/B,kBAAkB,EAAE,CAAC,MAAA,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,mCAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aACnE,CAAC;QACJ,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The graph routes the process runs with, as core's routing holds them once the app's storage\n * file has run (`LinkedStorage.setDatasetForShapes(dataset, shapes, {graph})`, or `loadStores`\n * for a datasets config that still carries routes). Answered by `GET /admin/routes`, so a host\n * can compare what it wrote into the storage file with what actually runs.\n */\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { getShapeGraphs, graphRoutingFor } from '@_linked/core/utils/graphRouting';\nimport { getAllShapeClasses } from '@_linked/core/utils/ShapeClass';\nimport { graphs } from '@_linked/core/ontologies/graphs';\nimport type { IDataset } from '@_linked/core/interfaces/IDataset';\n\n/** One dataset the router knows, and its graph routes. */\nexport interface DatasetRoutes {\n /**\n * The store's class name (`FusekiStore`, `RemoteDataset`, …). The server never sees the\n * aliases of the storage config, so a dataset is told apart by this, `default` and `id`.\n */\n store: string;\n /** The store's `id`, when it has one (a named `RemoteDataset` does). */\n id?: string;\n /** Whether this is the default dataset (the one for every shape without a pin). */\n default: boolean;\n /** The dataset's routes as registered: shape IRI → graph IRI. */\n routes: Record<string, string>;\n /**\n * The registered shapes that resolve to this dataset and whose instances are written to its\n * default graph: no route of their own and no routed super-shape. Sorted.\n */\n defaultGraphShapes: string[];\n}\n\n/** The body of `GET /admin/routes`. */\nexport interface EffectiveRoutes {\n /** Every dataset the router knows, the default one first, then in the order they were pinned. */\n datasets: DatasetRoutes[];\n}\n\n/** The routes of every dataset the router knows (see {@link EffectiveRoutes}). */\nexport function effectiveGraphRoutes(): EffectiveRoutes {\n const datasets = [...LinkedStorage.getDatasets()];\n const defaultDataset = LinkedStorage.getDefaultDataset();\n const defaultGraphShapes = new Map<IDataset, string[]>(datasets.map((ds) => [ds, []]));\n for (const [iri, shapeClass] of getAllShapeClasses()) {\n const dataset = LinkedStorage.getDatasetForShapeClass(shapeClass);\n const list = dataset && defaultGraphShapes.get(dataset);\n if (!list) continue;\n const graph = graphRoutingFor(dataset)?.graphForShape(iri) ?? graphs.default;\n if (graph === graphs.default) list.push(iri);\n }\n return {\n datasets: datasets.map((dataset) => {\n const id = (dataset as { id?: unknown }).id;\n return {\n store: dataset?.constructor?.name || 'unknown',\n ...(typeof id === 'string' && id ? { id } : {}),\n default: dataset === defaultDataset,\n routes: getShapeGraphs(dataset),\n defaultGraphShapes: (defaultGraphShapes.get(dataset) ?? []).sort(),\n };\n }),\n };\n}\n"]}
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* Ontology sync and the shape check, as the server runs them before its shape sync: at boot and
|
|
3
3
|
* when the host asks for a re-sync (`POST /admin/sync-shapes`).
|
|
4
4
|
*
|
|
5
|
-
* - The ontology sync (core `syncOntologies`) writes each registered package's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - The ontology sync (core `syncOntologies`) writes each registered package's ontology data into
|
|
6
|
+
* its own named graph of the app's default dataset (`{package IRI}/graph/ontology`, with a
|
|
7
|
+
* record in the graph registry that marks it as a hierarchy source), skipping a package whose
|
|
8
|
+
* stored hash matches, so the store's RDFS view infers the class hierarchy. A store without
|
|
9
|
+
* graph queries (it answers `unsupported-query-kind`, or has no `graphQuery`) gets core's
|
|
10
|
+
* earlier sync of the `rdfs:subClassOf` triples into the default graph instead. The graphs the
|
|
11
|
+
* sync wrote or dropped are observed on core's query dispatch and reported by name.
|
|
8
12
|
* - The shape check (core `checkShapeHierarchy`) compares the registered shapes with that
|
|
9
13
|
* hierarchy. A running app is never stopped by it: its findings are logged and counted.
|
|
10
14
|
* - The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded also when the sync
|
|
@@ -42,6 +46,14 @@ export interface OntologyCore {
|
|
|
42
46
|
}>>;
|
|
43
47
|
/** Loads the in-memory class hierarchy. Optional so a test double can leave it out. */
|
|
44
48
|
loadOntologyHierarchy?: () => Promise<unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Calls `onGraph` for every graph query on `ds` that succeeds until the returned function is
|
|
51
|
+
* called (core's `subscribeQueryDispatch`). Optional so a test double can leave it out.
|
|
52
|
+
*/
|
|
53
|
+
observeGraphQueries?: (ds: unknown, onGraph: (query: {
|
|
54
|
+
kind: string;
|
|
55
|
+
graph: string;
|
|
56
|
+
}, settled: Promise<unknown>) => void) => () => void;
|
|
45
57
|
/**
|
|
46
58
|
* Registers the ontologies of the installed packages in the given directories. Optional so a
|
|
47
59
|
* test double can leave it out.
|
|
@@ -52,10 +64,16 @@ export interface OntologyCore {
|
|
|
52
64
|
}
|
|
53
65
|
/** What the ontology sync and the shape check did, as `POST /admin/sync-shapes` reports it. */
|
|
54
66
|
export interface OntologyPhaseResult {
|
|
55
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Packages whose ontology graph (or, on a store without graph queries, hierarchy) was written,
|
|
69
|
+
* packages left as they were (unchanged or unreadable), and the graphs the sync wrote and
|
|
70
|
+
* dropped (empty on a store without graph queries).
|
|
71
|
+
*/
|
|
56
72
|
ontologies: {
|
|
57
73
|
written: string[];
|
|
58
74
|
skipped: string[];
|
|
75
|
+
graphs?: string[];
|
|
76
|
+
droppedGraphs?: string[];
|
|
59
77
|
};
|
|
60
78
|
/** The shape check's findings, counted by severity. */
|
|
61
79
|
shapeCheck: {
|
|
@@ -83,8 +101,8 @@ export declare function bootSyncsOntologies(env: Record<string, string | undefin
|
|
|
83
101
|
export declare function loadOntologyCore(): Promise<OntologyCore>;
|
|
84
102
|
/**
|
|
85
103
|
* The package directories whose ontologies register before the sync: every linked package
|
|
86
|
-
* discovered in the app's dependency tree, then the app's own workspace packages (
|
|
87
|
-
* `packages
|
|
104
|
+
* discovered in the app's dependency tree, then the app's own workspace packages (an ontology
|
|
105
|
+
* package in `packages/` that no package imports is one, so the dependency walk never reaches it).
|
|
88
106
|
* Order kept, duplicates dropped.
|
|
89
107
|
*/
|
|
90
108
|
export declare function ontologyPackageDirs(linkedPackageDirs: Iterable<string>, workspacePackageDirs: Iterable<string>): string[];
|
|
@@ -22,17 +22,22 @@ export async function loadOntologyCore() {
|
|
|
22
22
|
const check = await import('@_linked/core/shapes/checkShapeHierarchy');
|
|
23
23
|
const hierarchy = await import('@_linked/core/ontology/hierarchy');
|
|
24
24
|
const registerInstalled = await import('@_linked/core/ontology/registerInstalled');
|
|
25
|
+
const dispatch = await import('@_linked/core/queries/queryDispatch');
|
|
25
26
|
return {
|
|
26
27
|
syncOntologies: (ds, options) => sync.syncOntologies(ds, options),
|
|
27
28
|
checkShapeHierarchy: (options) => check.checkShapeHierarchy(options),
|
|
28
29
|
loadOntologyHierarchy: () => hierarchy.loadOntologyHierarchy(),
|
|
29
30
|
registerInstalledOntologies: (packageDirs, options) => registerInstalled.registerInstalledOntologies(packageDirs, options),
|
|
31
|
+
observeGraphQueries: (ds, onGraph) => dispatch.subscribeQueryDispatch((event) => {
|
|
32
|
+
if (event.kind === 'graph' && event.target === ds)
|
|
33
|
+
onGraph(event.query, event.result);
|
|
34
|
+
}),
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* The package directories whose ontologies register before the sync: every linked package
|
|
34
|
-
* discovered in the app's dependency tree, then the app's own workspace packages (
|
|
35
|
-
* `packages
|
|
39
|
+
* discovered in the app's dependency tree, then the app's own workspace packages (an ontology
|
|
40
|
+
* package in `packages/` that no package imports is one, so the dependency walk never reaches it).
|
|
36
41
|
* Order kept, duplicates dropped.
|
|
37
42
|
*/
|
|
38
43
|
export function ontologyPackageDirs(linkedPackageDirs, workspacePackageDirs) {
|
|
@@ -57,9 +62,9 @@ const list = (names) => (names.length ? names.join(', ') : 'none');
|
|
|
57
62
|
* the check and one line per finding.
|
|
58
63
|
*/
|
|
59
64
|
export async function runOntologyPhase(ds, { core, sync, orphanScope, dependencies, packageDirs = [], importModule, log = console.log, warn = console.warn, }) {
|
|
60
|
-
var _a;
|
|
65
|
+
var _a, _b;
|
|
61
66
|
const result = {
|
|
62
|
-
ontologies: { written: [], skipped: [] },
|
|
67
|
+
ontologies: { written: [], skipped: [], graphs: [], droppedGraphs: [] },
|
|
63
68
|
shapeCheck: { errors: 0, warnings: 0 },
|
|
64
69
|
};
|
|
65
70
|
if (core.registerInstalledOntologies && packageDirs.length) {
|
|
@@ -75,17 +80,33 @@ export async function runOntologyPhase(ds, { core, sync, orphanScope, dependenci
|
|
|
75
80
|
}
|
|
76
81
|
let hierarchyLoaded = false;
|
|
77
82
|
if (sync && ds) {
|
|
83
|
+
const written = new Set();
|
|
84
|
+
const dropped = new Set();
|
|
85
|
+
const pending = [];
|
|
86
|
+
const stop = (_a = core.observeGraphQueries) === null || _a === void 0 ? void 0 : _a.call(core, ds, (query, settled) => {
|
|
87
|
+
pending.push(settled.then(() => (query.kind === 'graph_drop' ? dropped : written).add(query.graph), () => undefined));
|
|
88
|
+
});
|
|
78
89
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
90
|
+
const sync = await core.syncOntologies(ds, { orphanScope });
|
|
91
|
+
await Promise.all(pending);
|
|
92
|
+
const graphs = [...written].sort();
|
|
93
|
+
const droppedGraphs = [...dropped].sort();
|
|
94
|
+
result.ontologies = { written: sync.written, skipped: sync.skipped, graphs, droppedGraphs };
|
|
81
95
|
hierarchyLoaded = true;
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
const defaultGraph = !!stop && sync.written.length > 0 && graphs.length === 0;
|
|
97
|
+
log(`[LinkedServer] ontology sync${defaultGraph ? ' (default graph: the store has no graph queries)' : ''}: ` +
|
|
98
|
+
`written ${list(sync.written)}; skipped ${list(sync.skipped)}` +
|
|
99
|
+
(orphanScope === 'all' ? `; pruned ${list(sync.pruned)}` : '') +
|
|
100
|
+
(graphs.length ? `; graphs written ${list(graphs)}` : '') +
|
|
101
|
+
(droppedGraphs.length ? `; graphs dropped ${list(droppedGraphs)}` : ''));
|
|
84
102
|
}
|
|
85
103
|
catch (err) {
|
|
86
104
|
result.failure = asError(err);
|
|
87
105
|
warn('[LinkedServer] ontology sync failed:', err);
|
|
88
106
|
}
|
|
107
|
+
finally {
|
|
108
|
+
stop === null || stop === void 0 ? void 0 : stop();
|
|
109
|
+
}
|
|
89
110
|
}
|
|
90
111
|
if (!hierarchyLoaded && core.loadOntologyHierarchy) {
|
|
91
112
|
try {
|
|
@@ -104,7 +125,7 @@ export async function runOntologyPhase(ds, { core, sync, orphanScope, dependenci
|
|
|
104
125
|
warn(`[LinkedServer] shape check ${f.severity} (${f.kind}): ${f.message}`);
|
|
105
126
|
}
|
|
106
127
|
catch (err) {
|
|
107
|
-
(
|
|
128
|
+
(_b = result.failure) !== null && _b !== void 0 ? _b : (result.failure = asError(err));
|
|
108
129
|
warn('[LinkedServer] shape check failed:', err);
|
|
109
130
|
}
|
|
110
131
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ontologySync.js","sourceRoot":"","sources":["../../../src/utils/ontologySync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ontology sync and the shape check, as the server runs them before its shape sync: at boot and\n * when the host asks for a re-sync (`POST /admin/sync-shapes`).\n *\n * - The ontology sync (core `syncOntologies`) writes each registered package's\n * `rdfs:subClassOf` triples into the app's default dataset, skipping a package whose stored\n * hash matches, so the store's RDFS view infers the class hierarchy.\n * - The shape check (core `checkShapeHierarchy`) compares the registered shapes with that\n * hierarchy. A running app is never stopped by it: its findings are logged and counted.\n * - The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded also when the sync\n * is off; the sync loads it itself when it runs.\n * - Before either, every linked package directory the server discovered registers its ontologies\n * (core `registerInstalledOntologies`): a package the app only deep-imports shape files from\n * never evaluates its register modules, so its data would be missing from both. A source\n * package without built register modules (a workspace package in dev) registers through\n * `importModule` (Vite's `ssrLoadModule`), when the caller passes one.\n *\n * The four core modules (`ontology/syncOntologies`, `shapes/checkShapeHierarchy`,\n * `ontology/hierarchy`, `ontology/registerInstalled`) are server only and not in core's index, so\n * {@link loadOntologyCore} imports them by their fixed subpaths when the phase runs; the core\n * this server depends on has all of them. Kept apart from `LinkedServer` so the order and the failure handling can be tested without\n * booting a server.\n */\nimport type {BootOrphanScope} from './syncShapes.js';\n\n/** The subset of core's ontology and shape-check API this module calls. */\nexport interface OntologyCore {\n syncOntologies(\n ds: unknown,\n options: {orphanScope: BootOrphanScope},\n ): Promise<{written: string[]; skipped: string[]; pruned: string[]}>;\n checkShapeHierarchy(options: {dependencies?: Record<string, string[]>}): Promise<\n Array<{kind: string; severity: 'error' | 'warning'; shape?: string; iri?: string; message: string}>\n >;\n /** Loads the in-memory class hierarchy. Optional so a test double can leave it out. */\n loadOntologyHierarchy?: () => Promise<unknown>;\n /**\n * Registers the ontologies of the installed packages in the given directories. Optional so a\n * test double can leave it out.\n */\n registerInstalledOntologies?: (\n packageDirs: string[],\n options?: {importModule?: (file: string) => Promise<unknown>},\n ) => Promise<string[]>;\n}\n\n/** What the ontology sync and the shape check did, as `POST /admin/sync-shapes` reports it. */\nexport interface OntologyPhaseResult {\n /** Packages whose hierarchy was written, and packages left as they were (unchanged or unreadable). */\n ontologies: {written: string[]; skipped: string[]};\n /** The shape check's findings, counted by severity. */\n shapeCheck: {errors: number; warnings: number};\n /** The first error the sync or the check threw; the phase went on without it. */\n failure?: Error;\n}\n\n/**\n * Whether the server syncs shapes (and so ontologies) into its app data: off when `linked.config`\n * sets `syncShapesOnBoot: false` or the environment sets `LINKED_SYNC_SHAPES_ON_BOOT=false`. One\n * switch for both syncs, at boot and on the host's request.\n */\nexport function shapeSyncOn(\n env: Record<string, string | undefined>,\n configFlag: unknown,\n): boolean {\n return configFlag !== false && env.LINKED_SYNC_SHAPES_ON_BOOT !== 'false';\n}\n\n/**\n * Whether the boot syncs ontologies: when shape sync is on ({@link shapeSyncOn}) and the\n * environment does not opt out with `LINKED_SYNC_ONTOLOGIES_ON_BOOT=false`.\n */\nexport function bootSyncsOntologies(\n env: Record<string, string | undefined>,\n configFlag?: unknown,\n): boolean {\n return shapeSyncOn(env, configFlag) && env.LINKED_SYNC_ONTOLOGIES_ON_BOOT !== 'false';\n}\n\n/**\n * Import core's ontology sync, shape check, hierarchy loader and installed packages'\n * registration.\n */\nexport async function loadOntologyCore(): Promise<OntologyCore> {\n const sync = await import('@_linked/core/ontology/syncOntologies');\n const check = await import('@_linked/core/shapes/checkShapeHierarchy');\n const hierarchy = await import('@_linked/core/ontology/hierarchy');\n const registerInstalled = await import('@_linked/core/ontology/registerInstalled');\n return {\n syncOntologies: (ds, options) =>\n sync.syncOntologies(ds as Parameters<typeof sync.syncOntologies>[0], options),\n checkShapeHierarchy: (options) => check.checkShapeHierarchy(options),\n loadOntologyHierarchy: () => hierarchy.loadOntologyHierarchy(),\n registerInstalledOntologies: (packageDirs, options) =>\n registerInstalled.registerInstalledOntologies(packageDirs, options),\n };\n}\n\n/**\n * The package directories whose ontologies register before the sync: every linked package\n * discovered in the app's dependency tree, then the app's own workspace packages (a CN clone's\n * `packages/<slug>-ont` is one no package imports, so the dependency walk never reaches it).\n * Order kept, duplicates dropped.\n */\nexport function ontologyPackageDirs(\n linkedPackageDirs: Iterable<string>,\n workspacePackageDirs: Iterable<string>,\n): string[] {\n return [...new Set([...linkedPackageDirs, ...workspacePackageDirs])];\n}\n\n/**\n * In dev, the loader for source register modules: Vite's `ssrLoadModule`, so a workspace package\n * that was never built registers on the core the SSR graph uses. Undefined without Vite (a built\n * app), where source packages are skipped.\n */\nexport function sourceModuleImporter(\n vite: {ssrLoadModule?: (url: string) => Promise<unknown>} | undefined | null,\n): ((file: string) => Promise<unknown>) | undefined {\n const ssrLoadModule = vite?.ssrLoadModule;\n return typeof ssrLoadModule === 'function' ? (file) => ssrLoadModule.call(vite, file) : undefined;\n}\n\nconst asError = (err: unknown): Error => (err instanceof Error ? err : new Error(String(err)));\nconst list = (names: string[]) => (names.length ? names.join(', ') : 'none');\n\n/**\n * Register the ontologies of the installed linked packages in `packageDirs` (package roots), sync\n * the registered ontologies into `ds` (when `sync` is set and there is a dataset), then run the\n * shape check. Never throws: a failed registration is logged and the phase goes on; a failed sync is logged and returned as `failure`, and the\n * check still runs; a failed check likewise. Logs one line for the sync, one summary line for\n * the check and one line per finding.\n */\nexport async function runOntologyPhase(\n ds: unknown,\n {\n core,\n sync,\n orphanScope,\n dependencies,\n packageDirs = [],\n importModule,\n log = console.log,\n warn = console.warn,\n }: {\n core: OntologyCore;\n sync: boolean;\n orphanScope: BootOrphanScope;\n dependencies?: Record<string, string[]>;\n /** Every linked package directory the server discovered (transitively). */\n packageDirs?: string[];\n /**\n * Imports a source register module (`src/ontologies/*.register.ts`) of a package directory\n * without built ones; in dev, Vite's `ssrLoadModule`.\n */\n importModule?: (file: string) => Promise<unknown>;\n log?: (message: string) => void;\n warn?: (message: string, err?: unknown) => void;\n },\n): Promise<OntologyPhaseResult> {\n const result: OntologyPhaseResult = {\n ontologies: {written: [], skipped: []},\n shapeCheck: {errors: 0, warnings: 0},\n };\n\n if (core.registerInstalledOntologies && packageDirs.length) {\n try {\n if (importModule) await core.registerInstalledOntologies(packageDirs, {importModule});\n else await core.registerInstalledOntologies(packageDirs);\n } catch (err) {\n warn(\"[LinkedServer] registering the installed packages' ontologies failed:\", err);\n }\n }\n\n let hierarchyLoaded = false;\n if (sync && ds) {\n try {\n const {written, skipped, pruned} = await core.syncOntologies(ds, {orphanScope});\n result.ontologies = {written, skipped};\n hierarchyLoaded = true;\n log(\n `[LinkedServer] ontology sync: written ${list(written)}; skipped ${list(skipped)}` +\n (orphanScope === 'all' ? `; pruned ${list(pruned)}` : ''),\n );\n } catch (err) {\n result.failure = asError(err);\n warn('[LinkedServer] ontology sync failed:', err);\n }\n }\n if (!hierarchyLoaded && core.loadOntologyHierarchy) {\n try {\n await core.loadOntologyHierarchy();\n } catch (err) {\n warn('[LinkedServer] loading the class hierarchy failed:', err);\n }\n }\n\n try {\n const findings = await core.checkShapeHierarchy({dependencies});\n const errors = findings.filter((f) => f.severity === 'error').length;\n result.shapeCheck = {errors, warnings: findings.length - errors};\n log(`[LinkedServer] shape check: ${errors} error(s), ${findings.length - errors} warning(s)`);\n for (const f of findings) warn(`[LinkedServer] shape check ${f.severity} (${f.kind}): ${f.message}`);\n } catch (err) {\n result.failure ??= asError(err);\n warn('[LinkedServer] shape check failed:', err);\n }\n return result;\n}\n\n/**\n * The host's re-sync: the ontology phase, then the shape sync, which runs also when the ontology\n * phase failed. The ontology phase's failure is then thrown, so the route answers with it, as\n * it does for a failed shape sync.\n */\nexport async function syncOntologiesThenShapes<T extends object>(\n ontologyPhase: () => Promise<OntologyPhaseResult>,\n shapeSync: () => Promise<T>,\n): Promise<T & Pick<OntologyPhaseResult, 'ontologies' | 'shapeCheck'>> {\n const {ontologies, shapeCheck, failure} = await ontologyPhase();\n const shapes = await shapeSync();\n if (failure) throw failure;\n return {...shapes, ontologies, shapeCheck};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ontologySync.js","sourceRoot":"","sources":["../../../src/utils/ontologySync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Ontology sync and the shape check, as the server runs them before its shape sync: at boot and\n * when the host asks for a re-sync (`POST /admin/sync-shapes`).\n *\n * - The ontology sync (core `syncOntologies`) writes each registered package's ontology data into\n * its own named graph of the app's default dataset (`{package IRI}/graph/ontology`, with a\n * record in the graph registry that marks it as a hierarchy source), skipping a package whose\n * stored hash matches, so the store's RDFS view infers the class hierarchy. A store without\n * graph queries (it answers `unsupported-query-kind`, or has no `graphQuery`) gets core's\n * earlier sync of the `rdfs:subClassOf` triples into the default graph instead. The graphs the\n * sync wrote or dropped are observed on core's query dispatch and reported by name.\n * - The shape check (core `checkShapeHierarchy`) compares the registered shapes with that\n * hierarchy. A running app is never stopped by it: its findings are logged and counted.\n * - The in-memory class hierarchy (core `loadOntologyHierarchy`) is loaded also when the sync\n * is off; the sync loads it itself when it runs.\n * - Before either, every linked package directory the server discovered registers its ontologies\n * (core `registerInstalledOntologies`): a package the app only deep-imports shape files from\n * never evaluates its register modules, so its data would be missing from both. A source\n * package without built register modules (a workspace package in dev) registers through\n * `importModule` (Vite's `ssrLoadModule`), when the caller passes one.\n *\n * The four core modules (`ontology/syncOntologies`, `shapes/checkShapeHierarchy`,\n * `ontology/hierarchy`, `ontology/registerInstalled`) are server only and not in core's index, so\n * {@link loadOntologyCore} imports them by their fixed subpaths when the phase runs; the core\n * this server depends on has all of them. Kept apart from `LinkedServer` so the order and the failure handling can be tested without\n * booting a server.\n */\nimport type {BootOrphanScope} from './syncShapes.js';\n\n/** The subset of core's ontology and shape-check API this module calls. */\nexport interface OntologyCore {\n syncOntologies(\n ds: unknown,\n options: {orphanScope: BootOrphanScope},\n ): Promise<{written: string[]; skipped: string[]; pruned: string[]}>;\n checkShapeHierarchy(options: {dependencies?: Record<string, string[]>}): Promise<\n Array<{kind: string; severity: 'error' | 'warning'; shape?: string; iri?: string; message: string}>\n >;\n /** Loads the in-memory class hierarchy. Optional so a test double can leave it out. */\n loadOntologyHierarchy?: () => Promise<unknown>;\n /**\n * Calls `onGraph` for every graph query on `ds` that succeeds until the returned function is\n * called (core's `subscribeQueryDispatch`). Optional so a test double can leave it out.\n */\n observeGraphQueries?: (\n ds: unknown,\n onGraph: (query: {kind: string; graph: string}, settled: Promise<unknown>) => void,\n ) => () => void;\n /**\n * Registers the ontologies of the installed packages in the given directories. Optional so a\n * test double can leave it out.\n */\n registerInstalledOntologies?: (\n packageDirs: string[],\n options?: {importModule?: (file: string) => Promise<unknown>},\n ) => Promise<string[]>;\n}\n\n/** What the ontology sync and the shape check did, as `POST /admin/sync-shapes` reports it. */\nexport interface OntologyPhaseResult {\n /**\n * Packages whose ontology graph (or, on a store without graph queries, hierarchy) was written,\n * packages left as they were (unchanged or unreadable), and the graphs the sync wrote and\n * dropped (empty on a store without graph queries).\n */\n ontologies: {written: string[]; skipped: string[]; graphs?: string[]; droppedGraphs?: string[]};\n /** The shape check's findings, counted by severity. */\n shapeCheck: {errors: number; warnings: number};\n /** The first error the sync or the check threw; the phase went on without it. */\n failure?: Error;\n}\n\n/**\n * Whether the server syncs shapes (and so ontologies) into its app data: off when `linked.config`\n * sets `syncShapesOnBoot: false` or the environment sets `LINKED_SYNC_SHAPES_ON_BOOT=false`. One\n * switch for both syncs, at boot and on the host's request.\n */\nexport function shapeSyncOn(\n env: Record<string, string | undefined>,\n configFlag: unknown,\n): boolean {\n return configFlag !== false && env.LINKED_SYNC_SHAPES_ON_BOOT !== 'false';\n}\n\n/**\n * Whether the boot syncs ontologies: when shape sync is on ({@link shapeSyncOn}) and the\n * environment does not opt out with `LINKED_SYNC_ONTOLOGIES_ON_BOOT=false`.\n */\nexport function bootSyncsOntologies(\n env: Record<string, string | undefined>,\n configFlag?: unknown,\n): boolean {\n return shapeSyncOn(env, configFlag) && env.LINKED_SYNC_ONTOLOGIES_ON_BOOT !== 'false';\n}\n\n/**\n * Import core's ontology sync, shape check, hierarchy loader and installed packages'\n * registration.\n */\nexport async function loadOntologyCore(): Promise<OntologyCore> {\n const sync = await import('@_linked/core/ontology/syncOntologies');\n const check = await import('@_linked/core/shapes/checkShapeHierarchy');\n const hierarchy = await import('@_linked/core/ontology/hierarchy');\n const registerInstalled = await import('@_linked/core/ontology/registerInstalled');\n const dispatch = await import('@_linked/core/queries/queryDispatch');\n return {\n syncOntologies: (ds, options) =>\n sync.syncOntologies(ds as Parameters<typeof sync.syncOntologies>[0], options),\n checkShapeHierarchy: (options) => check.checkShapeHierarchy(options),\n loadOntologyHierarchy: () => hierarchy.loadOntologyHierarchy(),\n registerInstalledOntologies: (packageDirs, options) =>\n registerInstalled.registerInstalledOntologies(packageDirs, options),\n observeGraphQueries: (ds, onGraph) =>\n dispatch.subscribeQueryDispatch((event) => {\n if (event.kind === 'graph' && event.target === ds) onGraph(event.query, event.result);\n }),\n };\n}\n\n/**\n * The package directories whose ontologies register before the sync: every linked package\n * discovered in the app's dependency tree, then the app's own workspace packages (an ontology\n * package in `packages/` that no package imports is one, so the dependency walk never reaches it).\n * Order kept, duplicates dropped.\n */\nexport function ontologyPackageDirs(\n linkedPackageDirs: Iterable<string>,\n workspacePackageDirs: Iterable<string>,\n): string[] {\n return [...new Set([...linkedPackageDirs, ...workspacePackageDirs])];\n}\n\n/**\n * In dev, the loader for source register modules: Vite's `ssrLoadModule`, so a workspace package\n * that was never built registers on the core the SSR graph uses. Undefined without Vite (a built\n * app), where source packages are skipped.\n */\nexport function sourceModuleImporter(\n vite: {ssrLoadModule?: (url: string) => Promise<unknown>} | undefined | null,\n): ((file: string) => Promise<unknown>) | undefined {\n const ssrLoadModule = vite?.ssrLoadModule;\n return typeof ssrLoadModule === 'function' ? (file) => ssrLoadModule.call(vite, file) : undefined;\n}\n\nconst asError = (err: unknown): Error => (err instanceof Error ? err : new Error(String(err)));\nconst list = (names: string[]) => (names.length ? names.join(', ') : 'none');\n\n/**\n * Register the ontologies of the installed linked packages in `packageDirs` (package roots), sync\n * the registered ontologies into `ds` (when `sync` is set and there is a dataset), then run the\n * shape check. Never throws: a failed registration is logged and the phase goes on; a failed sync is logged and returned as `failure`, and the\n * check still runs; a failed check likewise. Logs one line for the sync, one summary line for\n * the check and one line per finding.\n */\nexport async function runOntologyPhase(\n ds: unknown,\n {\n core,\n sync,\n orphanScope,\n dependencies,\n packageDirs = [],\n importModule,\n log = console.log,\n warn = console.warn,\n }: {\n core: OntologyCore;\n sync: boolean;\n orphanScope: BootOrphanScope;\n dependencies?: Record<string, string[]>;\n /** Every linked package directory the server discovered (transitively). */\n packageDirs?: string[];\n /**\n * Imports a source register module (`src/ontologies/*.register.ts`) of a package directory\n * without built ones; in dev, Vite's `ssrLoadModule`.\n */\n importModule?: (file: string) => Promise<unknown>;\n log?: (message: string) => void;\n warn?: (message: string, err?: unknown) => void;\n },\n): Promise<OntologyPhaseResult> {\n const result: OntologyPhaseResult = {\n ontologies: {written: [], skipped: [], graphs: [], droppedGraphs: []},\n shapeCheck: {errors: 0, warnings: 0},\n };\n\n if (core.registerInstalledOntologies && packageDirs.length) {\n try {\n if (importModule) await core.registerInstalledOntologies(packageDirs, {importModule});\n else await core.registerInstalledOntologies(packageDirs);\n } catch (err) {\n warn(\"[LinkedServer] registering the installed packages' ontologies failed:\", err);\n }\n }\n\n let hierarchyLoaded = false;\n if (sync && ds) {\n const written = new Set<string>();\n const dropped = new Set<string>();\n const pending: Promise<unknown>[] = [];\n const stop = core.observeGraphQueries?.(ds, (query, settled) => {\n pending.push(\n settled.then(\n () => (query.kind === 'graph_drop' ? dropped : written).add(query.graph),\n () => undefined,\n ),\n );\n });\n try {\n const sync = await core.syncOntologies(ds, {orphanScope});\n await Promise.all(pending);\n const graphs = [...written].sort();\n const droppedGraphs = [...dropped].sort();\n result.ontologies = {written: sync.written, skipped: sync.skipped, graphs, droppedGraphs};\n hierarchyLoaded = true;\n const defaultGraph = !!stop && sync.written.length > 0 && graphs.length === 0;\n log(\n `[LinkedServer] ontology sync${defaultGraph ? ' (default graph: the store has no graph queries)' : ''}: ` +\n `written ${list(sync.written)}; skipped ${list(sync.skipped)}` +\n (orphanScope === 'all' ? `; pruned ${list(sync.pruned)}` : '') +\n (graphs.length ? `; graphs written ${list(graphs)}` : '') +\n (droppedGraphs.length ? `; graphs dropped ${list(droppedGraphs)}` : ''),\n );\n } catch (err) {\n result.failure = asError(err);\n warn('[LinkedServer] ontology sync failed:', err);\n } finally {\n stop?.();\n }\n }\n if (!hierarchyLoaded && core.loadOntologyHierarchy) {\n try {\n await core.loadOntologyHierarchy();\n } catch (err) {\n warn('[LinkedServer] loading the class hierarchy failed:', err);\n }\n }\n\n try {\n const findings = await core.checkShapeHierarchy({dependencies});\n const errors = findings.filter((f) => f.severity === 'error').length;\n result.shapeCheck = {errors, warnings: findings.length - errors};\n log(`[LinkedServer] shape check: ${errors} error(s), ${findings.length - errors} warning(s)`);\n for (const f of findings) warn(`[LinkedServer] shape check ${f.severity} (${f.kind}): ${f.message}`);\n } catch (err) {\n result.failure ??= asError(err);\n warn('[LinkedServer] shape check failed:', err);\n }\n return result;\n}\n\n/**\n * The host's re-sync: the ontology phase, then the shape sync, which runs also when the ontology\n * phase failed. The ontology phase's failure is then thrown, so the route answers with it, as\n * it does for a failed shape sync.\n */\nexport async function syncOntologiesThenShapes<T extends object>(\n ontologyPhase: () => Promise<OntologyPhaseResult>,\n shapeSync: () => Promise<T>,\n): Promise<T & Pick<OntologyPhaseResult, 'ontologies' | 'shapeCheck'>> {\n const {ontologies, shapeCheck, failure} = await ontologyPhase();\n const shapes = await shapeSync();\n if (failure) throw failure;\n return {...shapes, ontologies, shapeCheck};\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GraphQuery } from '@_linked/core/queries/graphQuery';
|
|
1
2
|
import type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';
|
|
2
3
|
import { type QueryOperation } from '@_linked/server-utils/utils/QueryAccess';
|
|
3
4
|
/**
|
|
@@ -63,3 +64,23 @@ export declare function lowerForAccessCheck(builder: any): unknown;
|
|
|
63
64
|
export declare function authorizeGenericQuery(operation: QueryOperation, query: unknown, builder: any, endpoint: string): Promise<void>;
|
|
64
65
|
/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */
|
|
65
66
|
export declare function authorizeRawQuery(query: unknown, endpoint: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is
|
|
69
|
+
* not a valid graph query (the access check then refuses it with 400).
|
|
70
|
+
*/
|
|
71
|
+
export declare function toGraphQuery(json: unknown): GraphQuery | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its
|
|
74
|
+
* record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401
|
|
75
|
+
* without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No
|
|
76
|
+
* catalog is resolved for a graph query (its record shapes are core's built-ins), so the check
|
|
77
|
+
* resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function authorizeGraphQuery(query: unknown, ir: GraphQuery | undefined, endpoint: string): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a
|
|
82
|
+
* `ServerCallError` with the refusal's status and message and its `code`, which the call route
|
|
83
|
+
* sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other
|
|
84
|
+
* error is returned as is.
|
|
85
|
+
*/
|
|
86
|
+
export declare function graphQueryCallError(err: unknown): unknown;
|