@_linked/server 3.7.0 → 3.8.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 +45 -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/shapes/LinkedServer.d.ts +3 -3
- package/lib/esm/shapes/LinkedServer.js +8 -5
- 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 +24 -2
- package/lib/esm/utils/adminChannel.js +43 -2
- package/lib/esm/utils/adminChannel.js.map +1 -1
- package/lib/esm/utils/ontologySync.d.ts +22 -4
- package/lib/esm/utils/ontologySync.js +28 -7
- package/lib/esm/utils/ontologySync.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +21 -0
- package/lib/esm/utils/queryPlane.js +54 -0
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/package.json +3 -3
|
@@ -22,11 +22,16 @@ 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
|
/**
|
|
@@ -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":"AAwDA;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,GAAuC,EACvC,UAAmB;IAEnB,OAAO,UAAU,KAAK,KAAK,IAAI,GAAG,CAAC,0BAA0B,KAAK,OAAO,CAAC;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAuC,EACvC,UAAoB;IAEpB,OAAO,WAAW,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,GAAG,CAAC,8BAA8B,KAAK,OAAO,CAAC;AACxF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,uCAAuC,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,0CAA0C,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,kCAAkC,CAAC,CAAC;IACnE,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,0CAA0C,CAAC,CAAC;IACnF,OAAO;QACL,cAAc,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CAC9B,IAAI,CAAC,cAAc,CAAC,EAA+C,EAAE,OAAO,CAAC;QAC/E,mBAAmB,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,CAAC;QACpE,qBAAqB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,qBAAqB,EAAE;QAC9D,2BAA2B,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,CACpD,iBAAiB,CAAC,2BAA2B,CAAC,WAAW,EAAE,OAAO,CAAC;KACtE,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,iBAAmC,EACnC,oBAAsC;IAEtC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,iBAAiB,EAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAA4E;IAE5E,MAAM,aAAa,GAAG,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,aAAa,CAAC;IAC1C,OAAO,OAAO,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACpG,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,GAAY,EAAS,EAAE,CAAC,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/F,MAAM,IAAI,GAAG,CAAC,KAAe,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAAW,EACX,EACE,IAAI,EACJ,IAAI,EACJ,WAAW,EACX,YAAY,EACZ,WAAW,GAAG,EAAE,EAChB,YAAY,EACZ,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,IAAI,GAAG,OAAO,CAAC,IAAI,GAepB;;IAED,MAAM,MAAM,GAAwB;QAClC,UAAU,EAAE,EAAC,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAC;QACtC,UAAU,EAAE,EAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAC;KACrC,CAAC;IAEF,IAAI,IAAI,CAAC,2BAA2B,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;QAC3D,IAAI,CAAC;YACH,IAAI,YAAY;gBAAE,MAAM,IAAI,CAAC,2BAA2B,CAAC,WAAW,EAAE,EAAC,YAAY,EAAC,CAAC,CAAC;;gBACjF,MAAM,IAAI,CAAC,2BAA2B,CAAC,WAAW,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,uEAAuE,EAAE,GAAG,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAED,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,EAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAC,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,EAAC,WAAW,EAAC,CAAC,CAAC;YAChF,MAAM,CAAC,UAAU,GAAG,EAAC,OAAO,EAAE,OAAO,EAAC,CAAC;YACvC,eAAe,GAAG,IAAI,CAAC;YACvB,GAAG,CACD,yCAAyC,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,EAAE;gBAChF,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC5D,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,CAAC,sCAAsC,EAAE,GAAG,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IACD,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACnD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,oDAAoD,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,EAAC,YAAY,EAAC,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;QACrE,MAAM,CAAC,UAAU,GAAG,EAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAC,CAAC;QACjE,GAAG,CAAC,+BAA+B,MAAM,cAAc,QAAQ,CAAC,MAAM,GAAG,MAAM,aAAa,CAAC,CAAC;QAC9F,KAAK,MAAM,CAAC,IAAI,QAAQ;YAAE,IAAI,CAAC,8BAA8B,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACvG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAA,MAAM,CAAC,OAAO,oCAAd,MAAM,CAAC,OAAO,GAAK,OAAO,CAAC,GAAG,CAAC,EAAC;QAChC,IAAI,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,aAAiD,EACjD,SAA2B;IAE3B,MAAM,EAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAC,GAAG,MAAM,aAAa,EAAE,CAAC;IAChE,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAC;IACjC,IAAI,OAAO;QAAE,MAAM,OAAO,CAAC;IAC3B,OAAO,EAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAAU,EAAC,CAAC;AAC7C,CAAC","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 (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: [], 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;
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* module turns the incoming query into the builder the store runs and the IR
|
|
11
11
|
* the rules read.
|
|
12
12
|
*
|
|
13
|
+
* Graph queries (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`) arrive as plain
|
|
14
|
+
* data on `/call/@_linked/server/graphQuery`: {@link toGraphQuery} checks the JSON, the store's
|
|
15
|
+
* `graph` rule decides ({@link authorizeGraphQuery}), and a refusal of the store goes back with
|
|
16
|
+
* its code ({@link graphQueryCallError}).
|
|
17
|
+
*
|
|
13
18
|
* The host can supply the shape catalog a request's query is decoded in
|
|
14
19
|
* (`setQueryCatalogResolver`), so one server can resolve the same shape IRI
|
|
15
20
|
* differently per request (for example per project branch).
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
* the ground up (see QueryAccess).
|
|
19
24
|
*/
|
|
20
25
|
import { fromJSON } from '@_linked/core';
|
|
26
|
+
import { isGraphQueryError, parseGraphQuery, } from '@_linked/core/queries/graphQuery';
|
|
21
27
|
import { lower } from '@_linked/core/queries/lower';
|
|
22
28
|
import { checkQueryAccess, } from '@_linked/server-utils/utils/QueryAccess';
|
|
23
29
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
@@ -174,4 +180,52 @@ export async function authorizeRawQuery(query, endpoint) {
|
|
|
174
180
|
endpoint,
|
|
175
181
|
});
|
|
176
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is
|
|
185
|
+
* not a valid graph query (the access check then refuses it with 400).
|
|
186
|
+
*/
|
|
187
|
+
export function toGraphQuery(json) {
|
|
188
|
+
try {
|
|
189
|
+
return parseGraphQuery(json);
|
|
190
|
+
}
|
|
191
|
+
catch (_a) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its
|
|
197
|
+
* record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401
|
|
198
|
+
* without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No
|
|
199
|
+
* catalog is resolved for a graph query (its record shapes are core's built-ins), so the check
|
|
200
|
+
* resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.
|
|
201
|
+
*/
|
|
202
|
+
export async function authorizeGraphQuery(query, ir, endpoint) {
|
|
203
|
+
await checkQueryAccess({
|
|
204
|
+
operation: 'graph',
|
|
205
|
+
query,
|
|
206
|
+
ir,
|
|
207
|
+
mode: getRpcExposureMode(),
|
|
208
|
+
endpoint,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** The HTTP status each graph query refusal is answered with. */
|
|
212
|
+
const GRAPH_REFUSAL_STATUS = {
|
|
213
|
+
'unsupported-query-kind': 501,
|
|
214
|
+
'graph-name-read-only': 400,
|
|
215
|
+
'graph-not-registered': 409,
|
|
216
|
+
'graph-exists': 409,
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a
|
|
220
|
+
* `ServerCallError` with the refusal's status and message and its `code`, which the call route
|
|
221
|
+
* sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other
|
|
222
|
+
* error is returned as is.
|
|
223
|
+
*/
|
|
224
|
+
export function graphQueryCallError(err) {
|
|
225
|
+
var _a;
|
|
226
|
+
if (!isGraphQueryError(err))
|
|
227
|
+
return err;
|
|
228
|
+
const status = (_a = GRAPH_REFUSAL_STATUS[err.code]) !== null && _a !== void 0 ? _a : 400;
|
|
229
|
+
return Object.assign(new ServerCallError(status, err.message, { cause: err }), { code: err.code });
|
|
230
|
+
}
|
|
177
231
|
//# sourceMappingURL=queryPlane.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport { lower } from '@_linked/core/queries/lower';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\n/**\n * The host's resolver, shared by every copy of this module in the process: the host sets it\n * through one copy, and the query endpoint served by another must use it rather than decode in\n * the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({\n catalogResolver: undefined as QueryCatalogResolver | undefined,\n}));\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryPlaneState.catalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryPlaneState.catalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * Graph queries (`graph_create`, `graph_replace`, `graph_patch`, `graph_drop`) arrive as plain\n * data on `/call/@_linked/server/graphQuery`: {@link toGraphQuery} checks the JSON, the store's\n * `graph` rule decides ({@link authorizeGraphQuery}), and a refusal of the store goes back with\n * its code ({@link graphQueryCallError}).\n *\n * The host can supply the shape catalog a request's query is decoded in\n * (`setQueryCatalogResolver`), so one server can resolve the same shape IRI\n * differently per request (for example per project branch).\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport {\n isGraphQueryError,\n parseGraphQuery,\n type GraphQuery,\n type GraphQueryErrorCode,\n} from '@_linked/core/queries/graphQuery';\nimport { lower } from '@_linked/core/queries/lower';\nimport type { ShapeCatalog } from '@_linked/core/utils/ShapeCatalog';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\nimport { sharedState } from '@_linked/core/utils/sharedState';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * Supplies the shape catalog a generic query request is decoded in, from the\n * DSL-JSON the client sent (the request context is available through the\n * host's own means, e.g. `getCallContext`). `undefined` means the global\n * shape registry.\n */\nexport type QueryCatalogResolver = (\n query: unknown\n) => ShapeCatalog | undefined | Promise<ShapeCatalog | undefined>;\n\n/**\n * The host's resolver, shared by every copy of this module in the process: the host sets it\n * through one copy, and the query endpoint served by another must use it rather than decode in\n * the global registry. See https://github.com/linked-fw/cli/blob/main/docs/architecture/shared-state-and-error-brands.md.\n */\nconst queryPlaneState = sharedState('@_linked/server', 'queryPlane', () => ({\n catalogResolver: undefined as QueryCatalogResolver | undefined,\n}));\n\n/**\n * Set (or, with `undefined`, clear) the host's catalog resolver for the\n * generic query endpoints. Without one, every query decodes in the global\n * shape registry.\n */\nexport function setQueryCatalogResolver(resolver: QueryCatalogResolver | undefined): void {\n queryPlaneState.catalogResolver = resolver;\n}\n\n/** A live builder passed by backend code (never a plain object from JSON). */\nfunction isLiveBuilder(query: any): boolean {\n return (\n !!query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n );\n}\n\n/**\n * The catalog `query` is decoded in: the host resolver's answer, or\n * `undefined` (the global registry) without a resolver. A live builder already\n * carries its catalog, so the resolver is not asked for it.\n *\n * A `ServerCallError` the resolver throws is passed on as is (the host chose\n * the status); any other failure is logged and answers 500.\n */\nexport async function resolveQueryCatalog(query: unknown): Promise<ShapeCatalog | undefined> {\n const resolver = queryPlaneState.catalogResolver;\n if (!resolver || isLiveBuilder(query)) return undefined;\n try {\n return await resolver(query);\n } catch (error) {\n if (ServerCallError.is(error)) throw error;\n console.error('[queryPlane] the shape catalog resolver failed:', error);\n throw new ServerCallError(500, 'Shape catalog unavailable');\n }\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent, decoded in\n * `catalog` (`undefined`: the global registry, whatever catalog is active;\n * see `resolveQueryCatalog`). The builder keeps that catalog, so\n * the access-check lowering and the store's lowering both resolve against it.\n * The result is what the store runs, so the access check and the store see\n * the same query. A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and its shape\n * catalog: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: unknown, catalog?: ShapeCatalog): any {\n if (isLiveBuilder(query)) return query;\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query as any, { catalog });\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/**\n * The graph query `json` describes, checked with core's `parseGraphQuery`; `undefined` when it is\n * not a valid graph query (the access check then refuses it with 400).\n */\nexport function toGraphQuery(json: unknown): GraphQuery | undefined {\n try {\n return parseGraphQuery(json);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check a graph query against the `graph` rule of the store `LinkedStorage.graphQuery` routes its\n * record class to (`QueryAccess`, operation `graph`): no rule, or `'none'`, refuses it (403, 401\n * without a session); a malformed one answers 400. `ir` is {@link toGraphQuery}'s result. No\n * catalog is resolved for a graph query (its record shapes are core's built-ins), so the check\n * resolves the store in the global registry, where `LinkedStorage.graphQuery` then runs it.\n */\nexport async function authorizeGraphQuery(\n query: unknown,\n ir: GraphQuery | undefined,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation: 'graph',\n query,\n ir,\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** The HTTP status each graph query refusal is answered with. */\nconst GRAPH_REFUSAL_STATUS: Record<GraphQueryErrorCode, number> = {\n 'unsupported-query-kind': 501,\n 'graph-name-read-only': 400,\n 'graph-not-registered': 409,\n 'graph-exists': 409,\n};\n\n/**\n * A store's graph query refusal (`GraphQueryError`) as the answer the caller gets: a\n * `ServerCallError` with the refusal's status and message and its `code`, which the call route\n * sends as `{error, code}` (`RemoteDataset` turns it back into a `GraphQueryError`). Any other\n * error is returned as is.\n */\nexport function graphQueryCallError(err: unknown): unknown {\n if (!isGraphQueryError(err)) return err;\n const status = GRAPH_REFUSAL_STATUS[err.code] ?? 400;\n return Object.assign(new ServerCallError(status, err.message, { cause: err }), { code: err.code });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.8.0",
|
|
14
14
|
"packageManager": "npm@12.2.0",
|
|
15
15
|
"linkedPackage": true,
|
|
16
16
|
"type": "module",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@_linked/cli": "^1.54.0",
|
|
42
|
-
"@_linked/core": "^2.
|
|
43
|
-
"@_linked/server-utils": "^1.
|
|
42
|
+
"@_linked/core": "^2.38.0",
|
|
43
|
+
"@_linked/server-utils": "^1.16.0",
|
|
44
44
|
"autoprefixer": "^10.4.18",
|
|
45
45
|
"compression": "^1.7.4",
|
|
46
46
|
"cors": "^2.8.5",
|