@oml/server 0.14.16 → 0.15.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/README.md +8 -1
- package/out/auth/feature-gate.d.ts +20 -5
- package/out/auth/feature-gate.js +80 -24
- package/out/auth/feature-gate.js.map +1 -1
- package/out/auth/feature-policy.js +1 -0
- package/out/auth/feature-policy.js.map +1 -1
- package/out/lsp/language-server.js +3 -47
- package/out/lsp/language-server.js.map +1 -1
- package/out/lsp/protocol/reasoner-protocol.d.ts +8 -0
- package/out/lsp/protocol/reasoner-protocol.js +3 -0
- package/out/lsp/protocol/reasoner-protocol.js.map +1 -1
- package/out/rest/export.js +1 -3
- package/out/rest/export.js.map +1 -1
- package/out/rest/routes.d.ts +5 -0
- package/out/rest/routes.js +173 -91
- package/out/rest/routes.js.map +1 -1
- package/out/rest/server.d.ts +0 -2
- package/out/rest/server.js +713 -444
- package/out/rest/server.js.map +1 -1
- package/out/rest/shacl-index.d.ts +19 -0
- package/out/rest/shacl-index.js +166 -0
- package/out/rest/shacl-index.js.map +1 -0
- package/out/rest/template.js +54 -38
- package/out/rest/template.js.map +1 -1
- package/out/rest/validation.d.ts +2 -1
- package/out/rest/validation.js +198 -136
- package/out/rest/validation.js.map +1 -1
- package/out/server.js +100 -9
- package/out/server.js.map +1 -1
- package/package.json +7 -6
package/out/rest/server.js
CHANGED
|
@@ -14,20 +14,20 @@ import { DataFactory, Writer } from 'n3';
|
|
|
14
14
|
import uFuzzy from '@leeoniya/ufuzzy';
|
|
15
15
|
import { MarkdownHandlerRegistry, MarkdownPreviewRuntime, buildTemplateCatalog as buildNavigationTemplateCatalog, extractLeadingFrontMatter, resolveTemplateForNavigation, renderTemplate, MarkdownExecutor, } from '@oml/markdown';
|
|
16
16
|
import { STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE, STATIC_MARKDOWN_RUNTIME_CSS } from '@oml/markdown/static';
|
|
17
|
-
import { applyOmlUpdate, collectOntologyMembers, getIriForNode, getOntologyModelIndex, iriFragment, isDescription, isOntology, isVocabulary, tokenizeForFuzzy, } from '@oml/language';
|
|
17
|
+
import { applyOmlUpdate, collectOntologyMembers, getIriForNode, getOntologyModelIndex, iriFragment, isDescription, isDescriptionBundle, isOntology, isVocabulary, isVocabularyBundle, tokenizeForFuzzy, } from '@oml/language';
|
|
18
18
|
import { detectSparqlKind } from '@oml/owl';
|
|
19
19
|
import { exportAssertedWorkspace, exportWorkspace } from './export.js';
|
|
20
20
|
import { startOmlLanguageServer } from '../lsp/language-server.js';
|
|
21
21
|
import { createOpenApiSpec, dispatchRestOperation, resolveRestOperationId } from './routes.js';
|
|
22
22
|
import { buildTemplateCatalog, expandTemplateComposeBlocks, findFilesByExtension, frontMatterString, isTemplateMarkdownFile, normalizeContextOntologyIri, } from './template.js';
|
|
23
23
|
import { lintWorkspace, validateWorkspace } from './validation.js';
|
|
24
|
+
import { reindexWorkspaceShacl } from './shacl-index.js';
|
|
24
25
|
import { OmlAccessError, requiredFeatureForRestOperation, } from '../auth/feature-policy.js';
|
|
25
26
|
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
26
27
|
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
27
28
|
const DEFAULT_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
28
29
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
29
30
|
const require = createModuleRequire();
|
|
30
|
-
const REST_DEBUG_ENABLED = false;
|
|
31
31
|
const SWAGGER_ASSET_FILES = new Set([
|
|
32
32
|
'swagger-ui.css',
|
|
33
33
|
'swagger-ui-bundle.js',
|
|
@@ -43,6 +43,162 @@ const SWAGGER_CONTENT_TYPES = {
|
|
|
43
43
|
let swaggerAssetValidationError;
|
|
44
44
|
let markdownStaticEntryFileCache;
|
|
45
45
|
const swaggerAssetPathCache = new Map();
|
|
46
|
+
function positionToOffset(text, position) {
|
|
47
|
+
let line = 0;
|
|
48
|
+
let index = 0;
|
|
49
|
+
while (line < position.line && index < text.length) {
|
|
50
|
+
const nl = text.indexOf('\n', index);
|
|
51
|
+
if (nl < 0) {
|
|
52
|
+
return text.length;
|
|
53
|
+
}
|
|
54
|
+
index = nl + 1;
|
|
55
|
+
line += 1;
|
|
56
|
+
}
|
|
57
|
+
return Math.min(text.length, index + Math.max(0, position.character));
|
|
58
|
+
}
|
|
59
|
+
function applyTextEditsToContent(content, edits) {
|
|
60
|
+
const normalized = [...edits].sort((left, right) => {
|
|
61
|
+
const leftStart = left.range.start.line === right.range.start.line
|
|
62
|
+
? left.range.start.character - right.range.start.character
|
|
63
|
+
: left.range.start.line - right.range.start.line;
|
|
64
|
+
if (leftStart !== 0) {
|
|
65
|
+
return -leftStart;
|
|
66
|
+
}
|
|
67
|
+
const leftEnd = left.range.end.line === right.range.end.line
|
|
68
|
+
? left.range.end.character - right.range.end.character
|
|
69
|
+
: left.range.end.line - right.range.end.line;
|
|
70
|
+
return -leftEnd;
|
|
71
|
+
});
|
|
72
|
+
let current = content;
|
|
73
|
+
for (const edit of normalized) {
|
|
74
|
+
const start = positionToOffset(current, edit.range.start);
|
|
75
|
+
const end = positionToOffset(current, edit.range.end);
|
|
76
|
+
current = `${current.slice(0, start)}${edit.newText}${current.slice(end)}`;
|
|
77
|
+
}
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
async function applyWorkspaceEditToFiles(edit) {
|
|
81
|
+
const byUri = new Map();
|
|
82
|
+
const changedFiles = [];
|
|
83
|
+
if (edit.changes) {
|
|
84
|
+
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
85
|
+
byUri.set(uri, [...(byUri.get(uri) ?? []), ...edits]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
89
|
+
for (const change of edit.documentChanges) {
|
|
90
|
+
if (change?.kind === 'delete' && typeof change.uri === 'string') {
|
|
91
|
+
if (change.uri.startsWith('file://')) {
|
|
92
|
+
const filePath = fileURLToPath(change.uri);
|
|
93
|
+
await fs.rm(filePath, { force: true });
|
|
94
|
+
changedFiles.push(change.uri);
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const uri = change?.textDocument?.uri;
|
|
99
|
+
const edits = change?.edits;
|
|
100
|
+
if (!uri || !Array.isArray(edits)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
byUri.set(uri, [...(byUri.get(uri) ?? []), ...edits]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [uri, edits] of byUri.entries()) {
|
|
107
|
+
if (!uri.startsWith('file://')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const filePath = fileURLToPath(uri);
|
|
111
|
+
const current = await fs.readFile(filePath, 'utf-8').catch((error) => {
|
|
112
|
+
if (error?.code === 'ENOENT') {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
const next = applyTextEditsToContent(current, edits);
|
|
118
|
+
if (next !== current) {
|
|
119
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
120
|
+
await fs.writeFile(filePath, next, 'utf-8');
|
|
121
|
+
changedFiles.push(uri);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return changedFiles;
|
|
125
|
+
}
|
|
126
|
+
function collectWorkspaceEditChangedFiles(edit) {
|
|
127
|
+
const changed = new Set();
|
|
128
|
+
if (edit.changes) {
|
|
129
|
+
for (const uri of Object.keys(edit.changes)) {
|
|
130
|
+
if (uri.startsWith('file://')) {
|
|
131
|
+
changed.add(uri);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
136
|
+
for (const change of edit.documentChanges) {
|
|
137
|
+
if (change?.kind === 'delete' && typeof change?.uri === 'string' && change.uri.startsWith('file://')) {
|
|
138
|
+
changed.add(change.uri);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const uri = change?.textDocument?.uri;
|
|
142
|
+
if (typeof uri === 'string' && uri.startsWith('file://')) {
|
|
143
|
+
changed.add(uri);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...changed];
|
|
148
|
+
}
|
|
149
|
+
function collectWorkspaceEditReloadUris(edit) {
|
|
150
|
+
const uris = new Set();
|
|
151
|
+
if (edit.changes) {
|
|
152
|
+
for (const uri of Object.keys(edit.changes)) {
|
|
153
|
+
if (uri.startsWith('file://')) {
|
|
154
|
+
uris.add(uri);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(edit.documentChanges)) {
|
|
159
|
+
for (const change of edit.documentChanges) {
|
|
160
|
+
if (change?.kind === 'delete') {
|
|
161
|
+
if (typeof change?.uri === 'string' && change.uri.startsWith('file://')) {
|
|
162
|
+
uris.add(change.uri);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const uri = change?.textDocument?.uri;
|
|
167
|
+
if (typeof uri === 'string' && uri.startsWith('file://')) {
|
|
168
|
+
uris.add(uri);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [...uris].map((uri) => URI.parse(uri));
|
|
173
|
+
}
|
|
174
|
+
function normalizeUpdateOperation(operation) {
|
|
175
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
176
|
+
return operation;
|
|
177
|
+
}
|
|
178
|
+
const record = operation;
|
|
179
|
+
const keys = Object.keys(record);
|
|
180
|
+
if (keys.length !== 1) {
|
|
181
|
+
return operation;
|
|
182
|
+
}
|
|
183
|
+
const kind = keys[0];
|
|
184
|
+
const payload = record[kind];
|
|
185
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
186
|
+
return operation;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
kind,
|
|
190
|
+
...payload,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function normalizeUpdateRequest(params) {
|
|
194
|
+
const operations = Array.isArray(params.operations)
|
|
195
|
+
? params.operations.map((operation) => normalizeUpdateOperation(operation))
|
|
196
|
+
: [];
|
|
197
|
+
return {
|
|
198
|
+
...params,
|
|
199
|
+
operations,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
46
202
|
function createModuleRequire() {
|
|
47
203
|
if (typeof __filename === 'string' && path.isAbsolute(__filename)) {
|
|
48
204
|
return createRequire(__filename);
|
|
@@ -53,16 +209,6 @@ function createModuleRequire() {
|
|
|
53
209
|
}
|
|
54
210
|
throw new Error('Unable to initialize module-anchored require() in @oml/server REST runtime.');
|
|
55
211
|
}
|
|
56
|
-
function debugRest(event, details = {}) {
|
|
57
|
-
if (!REST_DEBUG_ENABLED) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const fields = Object.entries(details)
|
|
61
|
-
.map(([key, value]) => `${key}=${String(value)}`)
|
|
62
|
-
.join(' ');
|
|
63
|
-
const suffix = fields.length > 0 ? ` ${fields}` : '';
|
|
64
|
-
process.stderr.write(`[oml-rest][debug] ${new Date().toISOString()} pid=${process.pid} event=${event}${suffix}\n`);
|
|
65
|
-
}
|
|
66
212
|
const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
67
213
|
'table',
|
|
68
214
|
'tree',
|
|
@@ -74,6 +220,70 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
|
74
220
|
'matrix',
|
|
75
221
|
'table-editor'
|
|
76
222
|
]);
|
|
223
|
+
function getOntologiesFromRuntime(runtime) {
|
|
224
|
+
const reasoningService = runtime.Oml.reasoning.ReasoningService;
|
|
225
|
+
const importGraph = reasoningService.getImportGraph();
|
|
226
|
+
const ontologyIndex = getOntologyModelIndex(runtime.shared);
|
|
227
|
+
const langiumDocuments = runtime.shared.workspace.LangiumDocuments;
|
|
228
|
+
// Collect all unique model URIs from the import graph
|
|
229
|
+
const modelUris = new Set();
|
|
230
|
+
for (const [importer, importedSet] of importGraph.imports.entries()) {
|
|
231
|
+
modelUris.add(importer);
|
|
232
|
+
for (const imported of importedSet) {
|
|
233
|
+
modelUris.add(imported);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Also include model URIs that are importers but not imported by anyone
|
|
237
|
+
for (const importer of importGraph.importedBy.keys()) {
|
|
238
|
+
modelUris.add(importer);
|
|
239
|
+
}
|
|
240
|
+
const ontologies = [];
|
|
241
|
+
for (const modelUri of modelUris) {
|
|
242
|
+
// Get ontology IRI
|
|
243
|
+
const ontologyIri = ontologyIndex.resolveOntologyIri(modelUri);
|
|
244
|
+
if (!ontologyIri)
|
|
245
|
+
continue;
|
|
246
|
+
// Get oml:type by loading the document
|
|
247
|
+
let omlType = 'http://opencaesar.io/oml#Vocabulary'; // default
|
|
248
|
+
try {
|
|
249
|
+
const uri = URI.parse(modelUri);
|
|
250
|
+
const document = langiumDocuments.getDocument(uri);
|
|
251
|
+
if (document?.parseResult?.value) {
|
|
252
|
+
const ontology = document.parseResult.value;
|
|
253
|
+
if (isOntology(ontology)) {
|
|
254
|
+
if (isVocabulary(ontology)) {
|
|
255
|
+
omlType = 'http://opencaesar.io/oml#Vocabulary';
|
|
256
|
+
}
|
|
257
|
+
else if (isVocabularyBundle(ontology)) {
|
|
258
|
+
omlType = 'http://opencaesar.io/oml#VocabularyBundle';
|
|
259
|
+
}
|
|
260
|
+
else if (isDescription(ontology)) {
|
|
261
|
+
omlType = 'http://opencaesar.io/oml#Description';
|
|
262
|
+
}
|
|
263
|
+
else if (isDescriptionBundle(ontology)) {
|
|
264
|
+
omlType = 'http://opencaesar.io/oml#DescriptionBundle';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Keep default oml:type if document can't be loaded
|
|
271
|
+
}
|
|
272
|
+
// Get imports
|
|
273
|
+
const importedUris = importGraph.directImportsOf(modelUri);
|
|
274
|
+
const imports = [];
|
|
275
|
+
for (const importedUri of importedUris) {
|
|
276
|
+
const importedOntologyIri = ontologyIndex.resolveOntologyIri(importedUri);
|
|
277
|
+
if (importedOntologyIri) {
|
|
278
|
+
imports.push(importedOntologyIri);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
ontologies.push({ ontology: ontologyIri, modelUri, oml_type: omlType, imports });
|
|
282
|
+
}
|
|
283
|
+
// Sort by ontology IRI
|
|
284
|
+
ontologies.sort((a, b) => a.ontology.localeCompare(b.ontology));
|
|
285
|
+
return ontologies;
|
|
286
|
+
}
|
|
77
287
|
class RestHttpError extends Error {
|
|
78
288
|
constructor(statusCode, message) {
|
|
79
289
|
super(message);
|
|
@@ -253,34 +463,6 @@ function escapeHtml(value) {
|
|
|
253
463
|
.replace(/"/g, '"')
|
|
254
464
|
.replace(/'/g, ''');
|
|
255
465
|
}
|
|
256
|
-
async function listWorkspaceModelFiles(workspaceRoot) {
|
|
257
|
-
const entries = [];
|
|
258
|
-
const ignoredNames = new Set(['.git', 'node_modules', 'dist', 'build', 'out']);
|
|
259
|
-
const visit = async (currentDir) => {
|
|
260
|
-
const children = await fs.readdir(currentDir, { withFileTypes: true });
|
|
261
|
-
for (const child of children) {
|
|
262
|
-
const childPath = path.join(currentDir, child.name);
|
|
263
|
-
if (child.isDirectory()) {
|
|
264
|
-
if (ignoredNames.has(child.name) || child.name.startsWith('.')) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
await visit(childPath);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (!child.isFile() || !child.name.toLowerCase().endsWith('.oml')) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const relativePath = path.relative(workspaceRoot, childPath).split(path.sep).join('/');
|
|
274
|
-
entries.push({
|
|
275
|
-
path: relativePath,
|
|
276
|
-
uri: pathToFileURL(childPath).toString(),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
await visit(workspaceRoot);
|
|
281
|
-
entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
282
|
-
return entries;
|
|
283
|
-
}
|
|
284
466
|
function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
285
467
|
const escapedWorkspaceRoot = escapeHtml(defaultWorkspaceRoot);
|
|
286
468
|
return `<!DOCTYPE html>
|
|
@@ -799,8 +981,8 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
799
981
|
</div>
|
|
800
982
|
<div class="file-row">
|
|
801
983
|
<label class="field">
|
|
802
|
-
<span class="field-label">OML
|
|
803
|
-
<input id="
|
|
984
|
+
<span class="field-label">OML Ontology</span>
|
|
985
|
+
<input id="ontologyIri" type="text" spellcheck="false" placeholder="Select an OML ontology from Browse" readonly>
|
|
804
986
|
</label>
|
|
805
987
|
<button id="browseModelsButton" class="ghost" type="button">Browse</button>
|
|
806
988
|
</div>
|
|
@@ -859,13 +1041,13 @@ LIMIT 25</textarea>
|
|
|
859
1041
|
<form method="dialog">
|
|
860
1042
|
<div class="dialog-header">
|
|
861
1043
|
<div>
|
|
862
|
-
<div class="dialog-title">Workspace OML
|
|
863
|
-
<div class="dialog-subtitle">Select the
|
|
1044
|
+
<div class="dialog-title">Workspace OML Ontologies</div>
|
|
1045
|
+
<div class="dialog-subtitle">Select the ontology to run the query against.</div>
|
|
864
1046
|
</div>
|
|
865
1047
|
<button id="closeBrowseDialog" type="submit" class="ghost">Close</button>
|
|
866
1048
|
</div>
|
|
867
1049
|
<div class="dialog-body">
|
|
868
|
-
<input id="browseFilter" type="text" placeholder="Filter
|
|
1050
|
+
<input id="browseFilter" type="text" placeholder="Filter ontologies by IRI" spellcheck="false">
|
|
869
1051
|
<div id="browseStatus" class="status">Loading...</div>
|
|
870
1052
|
<div id="browseList" class="browse-list"></div>
|
|
871
1053
|
</div>
|
|
@@ -873,7 +1055,7 @@ LIMIT 25</textarea>
|
|
|
873
1055
|
</dialog>
|
|
874
1056
|
|
|
875
1057
|
<script>
|
|
876
|
-
const
|
|
1058
|
+
const ontologyIriInput = document.getElementById('ontologyIri');
|
|
877
1059
|
const sparqlInput = document.getElementById('sparql');
|
|
878
1060
|
const runButton = document.getElementById('runQueryButton');
|
|
879
1061
|
const browseModelsButton = document.getElementById('browseModelsButton');
|
|
@@ -891,7 +1073,7 @@ LIMIT 25</textarea>
|
|
|
891
1073
|
const browseFilter = document.getElementById('browseFilter');
|
|
892
1074
|
|
|
893
1075
|
let browseEntries = [];
|
|
894
|
-
let
|
|
1076
|
+
let selectedOntologyIri = '';
|
|
895
1077
|
let currentTableColumns = [];
|
|
896
1078
|
let currentTableRows = [];
|
|
897
1079
|
let currentPageIndex = 0;
|
|
@@ -903,7 +1085,10 @@ LIMIT 25</textarea>
|
|
|
903
1085
|
}
|
|
904
1086
|
|
|
905
1087
|
function routeUrl(relativePath) {
|
|
906
|
-
|
|
1088
|
+
let normalized = String(relativePath || '');
|
|
1089
|
+
while (normalized.startsWith('/')) {
|
|
1090
|
+
normalized = normalized.slice(1);
|
|
1091
|
+
}
|
|
907
1092
|
return baseRoutePath() + normalized;
|
|
908
1093
|
}
|
|
909
1094
|
|
|
@@ -992,18 +1177,30 @@ LIMIT 25</textarea>
|
|
|
992
1177
|
renderTablePage();
|
|
993
1178
|
}
|
|
994
1179
|
|
|
1180
|
+
function ontologyIriToModelId(iri) {
|
|
1181
|
+
try {
|
|
1182
|
+
const url = new URL(iri);
|
|
1183
|
+
const rawPath = url.pathname.replace(/[/]+$/, '');
|
|
1184
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
1185
|
+
const stem = segments[segments.length - 1] || 'index';
|
|
1186
|
+
const dirSegs = url.host ? [url.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
|
|
1187
|
+
return [...dirSegs, stem].join('/');
|
|
1188
|
+
} catch {
|
|
1189
|
+
return iri;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
995
1193
|
function renderBrowseEntries(entries) {
|
|
996
1194
|
if (entries.length === 0) {
|
|
997
|
-
browseList.innerHTML = '<div class="empty">No OML
|
|
1195
|
+
browseList.innerHTML = '<div class="empty">No OML ontologies found for the current filter.</div>';
|
|
998
1196
|
return;
|
|
999
1197
|
}
|
|
1000
1198
|
browseList.innerHTML = entries.map((entry) => {
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
const isSelected = selectedModelId && selectedModelId === modelId;
|
|
1199
|
+
const iri = String(entry.ontology || '').trim();
|
|
1200
|
+
const isSelected = selectedOntologyIri && selectedOntologyIri === iri;
|
|
1004
1201
|
const selectedClass = isSelected ? ' selected' : '';
|
|
1005
|
-
return '<button type="button" class="browse-entry' + selectedClass + '" data-
|
|
1006
|
-
+ '<div class="browse-entry-path" title="' + escapeClientHtml(
|
|
1202
|
+
return '<button type="button" class="browse-entry' + selectedClass + '" data-ontology-iri="' + escapeClientHtml(iri) + '">'
|
|
1203
|
+
+ '<div class="browse-entry-path" title="' + escapeClientHtml(iri) + '">' + escapeClientHtml(iri) + '</div>'
|
|
1007
1204
|
+ '</button>';
|
|
1008
1205
|
}).join('');
|
|
1009
1206
|
}
|
|
@@ -1012,52 +1209,50 @@ LIMIT 25</textarea>
|
|
|
1012
1209
|
const filter = (browseFilter.value || '').trim().toLowerCase();
|
|
1013
1210
|
const filtered = filter.length === 0
|
|
1014
1211
|
? browseEntries
|
|
1015
|
-
: browseEntries.filter((entry) => entry.
|
|
1212
|
+
: browseEntries.filter((entry) => String(entry.ontology || '').toLowerCase().includes(filter));
|
|
1016
1213
|
renderBrowseEntries(filtered);
|
|
1017
1214
|
}
|
|
1018
1215
|
|
|
1019
1216
|
async function loadWorkspaceModels() {
|
|
1020
|
-
browseStatus.textContent = 'Loading workspace OML
|
|
1217
|
+
browseStatus.textContent = 'Loading workspace OML ontologies...';
|
|
1021
1218
|
browseStatus.className = 'status';
|
|
1022
1219
|
try {
|
|
1023
|
-
const response = await fetch(routeUrl('v0/
|
|
1220
|
+
const response = await fetch(routeUrl('v0/ontologies'));
|
|
1024
1221
|
const payload = await response.json();
|
|
1025
1222
|
if (!response.ok) {
|
|
1026
|
-
throw new Error(payload.error || 'Unable to list workspace OML
|
|
1223
|
+
throw new Error(payload.error || 'Unable to list workspace OML ontologies.');
|
|
1027
1224
|
}
|
|
1028
|
-
browseEntries = Array.isArray(payload.
|
|
1029
|
-
browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace
|
|
1225
|
+
browseEntries = Array.isArray(payload.ontologies) ? payload.ontologies : [];
|
|
1226
|
+
browseStatus.textContent = 'Loaded ' + browseEntries.length + ' workspace ontolog' + (browseEntries.length === 1 ? 'y' : 'ies') + '.';
|
|
1030
1227
|
browseStatus.className = 'status success';
|
|
1031
1228
|
applyBrowseFilter();
|
|
1032
1229
|
} catch (error) {
|
|
1033
1230
|
browseEntries = [];
|
|
1034
|
-
browseList.innerHTML = '<div class="empty">Unable to load workspace OML
|
|
1231
|
+
browseList.innerHTML = '<div class="empty">Unable to load workspace OML ontologies.</div>';
|
|
1035
1232
|
browseStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
1036
1233
|
browseStatus.className = 'status error';
|
|
1037
1234
|
}
|
|
1038
1235
|
}
|
|
1039
1236
|
|
|
1040
|
-
function
|
|
1041
|
-
if (!
|
|
1042
|
-
return 'OML
|
|
1043
|
-
}
|
|
1044
|
-
if (modelId.toLowerCase().endsWith('.md')) {
|
|
1045
|
-
return 'Markdown files are not queryable directly. Choose an .oml file.';
|
|
1237
|
+
function validateOntologyIri(iri) {
|
|
1238
|
+
if (!iri) {
|
|
1239
|
+
return 'OML ontology is required.';
|
|
1046
1240
|
}
|
|
1047
1241
|
return undefined;
|
|
1048
1242
|
}
|
|
1049
1243
|
|
|
1050
1244
|
async function executeQuery() {
|
|
1051
|
-
const
|
|
1245
|
+
const iri = selectedOntologyIri.trim();
|
|
1052
1246
|
const sparql = sparqlInput.value.trim();
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
setStatus(
|
|
1056
|
-
rawOutput.textContent = JSON.stringify({ error:
|
|
1057
|
-
resetTable('Choose a valid OML
|
|
1247
|
+
const iriError = validateOntologyIri(iri);
|
|
1248
|
+
if (iriError) {
|
|
1249
|
+
setStatus(iriError, 'error');
|
|
1250
|
+
rawOutput.textContent = JSON.stringify({ error: iriError }, null, 2);
|
|
1251
|
+
resetTable('Choose a valid OML ontology to run a query.');
|
|
1058
1252
|
browseModelsButton.focus();
|
|
1059
1253
|
return;
|
|
1060
1254
|
}
|
|
1255
|
+
const modelId = ontologyIriToModelId(iri);
|
|
1061
1256
|
if (!sparql) {
|
|
1062
1257
|
setStatus('Query is required.', 'error');
|
|
1063
1258
|
rawOutput.textContent = JSON.stringify({ error: 'Query is required.' }, null, 2);
|
|
@@ -1137,18 +1332,17 @@ LIMIT 25</textarea>
|
|
|
1137
1332
|
});
|
|
1138
1333
|
|
|
1139
1334
|
browseList.addEventListener('click', (event) => {
|
|
1140
|
-
const target = event.target && event.target.closest ? event.target.closest('button[data-
|
|
1335
|
+
const target = event.target && event.target.closest ? event.target.closest('button[data-ontology-iri]') : null;
|
|
1141
1336
|
if (!target) {
|
|
1142
1337
|
return;
|
|
1143
1338
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
if (!modelId) {
|
|
1339
|
+
const iri = target.getAttribute('data-ontology-iri');
|
|
1340
|
+
if (!iri) {
|
|
1147
1341
|
return;
|
|
1148
1342
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
setStatus('Selected
|
|
1343
|
+
selectedOntologyIri = iri;
|
|
1344
|
+
ontologyIriInput.value = iri;
|
|
1345
|
+
setStatus('Selected OML ontology.', 'success');
|
|
1152
1346
|
applyBrowseFilter();
|
|
1153
1347
|
if (browseDialog.open) {
|
|
1154
1348
|
browseDialog.close();
|
|
@@ -1223,27 +1417,41 @@ function resolveSparqlModelPathFromRoute(pathname) {
|
|
|
1223
1417
|
const modelPath = decodeURIComponent(pathname.slice(prefix.length)).trim();
|
|
1224
1418
|
return modelPath.length > 0 ? modelPath : undefined;
|
|
1225
1419
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1420
|
+
/**
|
|
1421
|
+
* Normalise a model reference (full `file://` URI or workspace-relative path,
|
|
1422
|
+
* optionally with `.oml` extension) to a canonical `file://` URI.
|
|
1423
|
+
* Throws a plain Error if the path escapes the workspace root.
|
|
1424
|
+
*/
|
|
1425
|
+
function ontologyIriToModelId(ontologyIri) {
|
|
1426
|
+
const iri = new URL(ontologyIri);
|
|
1427
|
+
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
1428
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
1429
|
+
const stem = segments.at(-1) || 'index';
|
|
1430
|
+
const dirSegs = iri.host ? [iri.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
|
|
1431
|
+
return [...dirSegs, stem].join('/');
|
|
1432
|
+
}
|
|
1433
|
+
function resolveModelUriFromIriModelId(docs, ontologyIndex, modelId) {
|
|
1434
|
+
const normalized = modelId.trim().replace(/^\/+|\/+$/g, '');
|
|
1228
1435
|
if (!normalized) {
|
|
1229
|
-
throw new RestHttpError(400, 'Missing model
|
|
1230
|
-
}
|
|
1231
|
-
if (path.isAbsolute(normalized)) {
|
|
1232
|
-
throw new RestHttpError(400, 'SPARQL model path must be workspace-relative.');
|
|
1233
|
-
}
|
|
1234
|
-
if (normalized.toLowerCase().endsWith('.oml')) {
|
|
1235
|
-
throw new RestHttpError(400, "SPARQL model path must not include '.oml'. Use workspace-relative path without extension.");
|
|
1436
|
+
throw new RestHttpError(400, 'Missing model ID in SPARQL endpoint route.');
|
|
1236
1437
|
}
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1438
|
+
for (const doc of docs) {
|
|
1439
|
+
const modelUri = String(doc?.uri ?? '').trim();
|
|
1440
|
+
if (!modelUri)
|
|
1441
|
+
continue;
|
|
1442
|
+
const ontologyIri = ontologyIndex.resolveOntologyIri?.(modelUri);
|
|
1443
|
+
if (!ontologyIri)
|
|
1444
|
+
continue;
|
|
1445
|
+
try {
|
|
1446
|
+
const derived = ontologyIriToModelId(ontologyIri);
|
|
1447
|
+
if (derived === normalized)
|
|
1448
|
+
return modelUri;
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
// skip unparseable IRIs
|
|
1452
|
+
}
|
|
1241
1453
|
}
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
async function assertModelUriExists(modelUri) {
|
|
1245
|
-
const filePath = fileURLToPath(modelUri);
|
|
1246
|
-
await fs.access(filePath, fsSync.constants.F_OK);
|
|
1454
|
+
throw new RestHttpError(404, `No OML ontology found for model ID '${normalized}'.`);
|
|
1247
1455
|
}
|
|
1248
1456
|
function parseSparqlQueryFromRequest(req, parsedUrl, bodyText) {
|
|
1249
1457
|
const method = (req.method ?? 'GET').toUpperCase();
|
|
@@ -1363,14 +1571,7 @@ function normalizeOntologyNamespace(value) {
|
|
|
1363
1571
|
return normalized.length > 0 ? normalized : undefined;
|
|
1364
1572
|
}
|
|
1365
1573
|
function resolveOutputPathFromOntologyIriString(ontologyIri, format) {
|
|
1366
|
-
|
|
1367
|
-
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
1368
|
-
const pathSegments = rawPath.split('/').filter(Boolean);
|
|
1369
|
-
const fileStem = pathSegments.at(-1) || 'index';
|
|
1370
|
-
const directorySegments = iri.host
|
|
1371
|
-
? [iri.host, ...pathSegments.slice(0, -1)]
|
|
1372
|
-
: pathSegments.slice(0, -1);
|
|
1373
|
-
return path.join(...directorySegments, `${fileStem}.${format}`);
|
|
1574
|
+
return ontologyIriToModelId(ontologyIri).split('/').join(path.sep) + '.' + format;
|
|
1374
1575
|
}
|
|
1375
1576
|
function normalizeRdfSerializationFormat(value) {
|
|
1376
1577
|
const normalized = String(value ?? 'ttl').trim().toLowerCase();
|
|
@@ -1379,7 +1580,27 @@ function normalizeRdfSerializationFormat(value) {
|
|
|
1379
1580
|
}
|
|
1380
1581
|
return 'ttl';
|
|
1381
1582
|
}
|
|
1382
|
-
|
|
1583
|
+
function filterPrefixesForQuads(quads, workspacePrefixes) {
|
|
1584
|
+
const usedNs = new Set();
|
|
1585
|
+
for (const quad of quads) {
|
|
1586
|
+
for (const term of [quad.subject, quad.predicate, quad.object]) {
|
|
1587
|
+
if (term?.termType === 'NamedNode') {
|
|
1588
|
+
const iri = term.value;
|
|
1589
|
+
const h = iri.lastIndexOf('#');
|
|
1590
|
+
const ns = h >= 0 ? iri.slice(0, h + 1) : iri.slice(0, iri.lastIndexOf('/') + 1);
|
|
1591
|
+
if (ns)
|
|
1592
|
+
usedNs.add(ns);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const result = {};
|
|
1597
|
+
for (const [px, ns] of Object.entries(workspacePrefixes)) {
|
|
1598
|
+
if (usedNs.has(ns))
|
|
1599
|
+
result[px] = ns;
|
|
1600
|
+
}
|
|
1601
|
+
return result;
|
|
1602
|
+
}
|
|
1603
|
+
async function serializeQuads(quads, format, pretty = false, prefixes) {
|
|
1383
1604
|
const writer = new Writer({
|
|
1384
1605
|
format: format === 'ttl'
|
|
1385
1606
|
? 'text/turtle'
|
|
@@ -1388,6 +1609,7 @@ async function serializeQuads(quads, format, pretty = false) {
|
|
|
1388
1609
|
: (format === 'nt'
|
|
1389
1610
|
? 'N-Triples'
|
|
1390
1611
|
: (format === 'nq' ? 'N-Quads' : 'application/n3'))),
|
|
1612
|
+
...(prefixes && Object.keys(prefixes).length > 0 ? { prefixes } : {}),
|
|
1391
1613
|
});
|
|
1392
1614
|
writer.addQuads(quads);
|
|
1393
1615
|
const serialized = await new Promise((resolve, reject) => {
|
|
@@ -1652,11 +1874,12 @@ function sanitizeBlockId(value) {
|
|
|
1652
1874
|
return trimmed.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1653
1875
|
}
|
|
1654
1876
|
async function writeBlockArtifacts(outputFile, results) {
|
|
1877
|
+
const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
|
|
1878
|
+
const blockDir = path.join(path.dirname(outputFile), blockDirName);
|
|
1879
|
+
await fs.rm(blockDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
1655
1880
|
if (results.length === 0) {
|
|
1656
1881
|
return { count: 0, manifest: [] };
|
|
1657
1882
|
}
|
|
1658
|
-
const blockDirName = `${path.basename(outputFile, '.html')}.blocks`;
|
|
1659
|
-
const blockDir = path.join(path.dirname(outputFile), blockDirName);
|
|
1660
1883
|
await fs.mkdir(blockDir, { recursive: true });
|
|
1661
1884
|
const manifest = [];
|
|
1662
1885
|
for (const result of results) {
|
|
@@ -1851,7 +2074,7 @@ ${content}
|
|
|
1851
2074
|
`;
|
|
1852
2075
|
}
|
|
1853
2076
|
class InMemoryJsonRpcLspClient {
|
|
1854
|
-
constructor(workspaceRoot, requestTimeoutMs,
|
|
2077
|
+
constructor(workspaceRoot, requestTimeoutMs, runtime) {
|
|
1855
2078
|
this.watchers = new Set();
|
|
1856
2079
|
this.watcherActive = false;
|
|
1857
2080
|
this.refreshInFlight = false;
|
|
@@ -1860,13 +2083,6 @@ class InMemoryJsonRpcLspClient {
|
|
|
1860
2083
|
this.workspaceRoot = workspaceRoot;
|
|
1861
2084
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
1862
2085
|
this.useExternalRuntime = runtime !== undefined;
|
|
1863
|
-
this.watchWorkspace = runtime ? false : watchWorkspace;
|
|
1864
|
-
debugRest('client.constructor', {
|
|
1865
|
-
workspaceRoot: path.resolve(workspaceRoot),
|
|
1866
|
-
requestTimeoutMs,
|
|
1867
|
-
useExternalRuntime: this.useExternalRuntime,
|
|
1868
|
-
watchWorkspace: this.watchWorkspace,
|
|
1869
|
-
});
|
|
1870
2086
|
if (runtime) {
|
|
1871
2087
|
this.runtime = runtime;
|
|
1872
2088
|
}
|
|
@@ -1883,9 +2099,17 @@ class InMemoryJsonRpcLspClient {
|
|
|
1883
2099
|
this.clientConnection = createConnection(this.serverToClient, this.clientToServer);
|
|
1884
2100
|
this.clientConnection.listen();
|
|
1885
2101
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
2102
|
+
this.startWorkspaceWatcher();
|
|
2103
|
+
}
|
|
2104
|
+
getWorkspacePrefixes() {
|
|
2105
|
+
const prefixes = {};
|
|
2106
|
+
for (const doc of this.getWorkspaceOmlDocuments()) {
|
|
2107
|
+
const root = doc?.parseResult?.value;
|
|
2108
|
+
if (root?.namespace && root?.prefix) {
|
|
2109
|
+
prefixes[root.prefix] = root.namespace;
|
|
2110
|
+
}
|
|
1888
2111
|
}
|
|
2112
|
+
return prefixes;
|
|
1889
2113
|
}
|
|
1890
2114
|
getWorkspaceOmlDocuments() {
|
|
1891
2115
|
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
@@ -1897,206 +2121,38 @@ class InMemoryJsonRpcLspClient {
|
|
|
1897
2121
|
.filter((doc) => String(doc?.uri ?? '').trim().toLowerCase().endsWith('.oml'))
|
|
1898
2122
|
.sort((left, right) => String(left?.uri ?? '').localeCompare(String(right?.uri ?? '')));
|
|
1899
2123
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
let errors = 0;
|
|
1903
|
-
let warnings = 0;
|
|
1904
|
-
let unresolved = 0;
|
|
1905
|
-
const unresolvedSamples = [];
|
|
1906
|
-
for (const doc of docs) {
|
|
1907
|
-
const uri = String(doc?.uri ?? '').trim();
|
|
1908
|
-
const docDiagnostics = Array.isArray(doc?.diagnostics) ? doc.diagnostics : [];
|
|
1909
|
-
for (const diagnostic of docDiagnostics) {
|
|
1910
|
-
diagnostics += 1;
|
|
1911
|
-
const severity = Number(diagnostic?.severity ?? 0);
|
|
1912
|
-
if (severity === 1) {
|
|
1913
|
-
errors += 1;
|
|
1914
|
-
}
|
|
1915
|
-
else if (severity === 2) {
|
|
1916
|
-
warnings += 1;
|
|
1917
|
-
}
|
|
1918
|
-
const message = String(diagnostic?.message ?? '');
|
|
1919
|
-
if (message.includes('Could not resolve reference')) {
|
|
1920
|
-
unresolved += 1;
|
|
1921
|
-
if (unresolvedSamples.length < 8) {
|
|
1922
|
-
const line = Number(diagnostic?.range?.start?.line ?? 0) + 1;
|
|
1923
|
-
const column = Number(diagnostic?.range?.start?.character ?? 0) + 1;
|
|
1924
|
-
unresolvedSamples.push(`${uri}:${line}:${column}:${message}`);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
return {
|
|
1930
|
-
docs: docs.length,
|
|
1931
|
-
diagnostics,
|
|
1932
|
-
errors,
|
|
1933
|
-
warnings,
|
|
1934
|
-
unresolved,
|
|
1935
|
-
unresolvedSamples,
|
|
1936
|
-
};
|
|
2124
|
+
async waitForValidatedWithTrace() {
|
|
2125
|
+
await this.runtime.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated);
|
|
1937
2126
|
}
|
|
1938
|
-
|
|
1939
|
-
const summary = {
|
|
1940
|
-
changed: 0,
|
|
1941
|
-
parsed: 0,
|
|
1942
|
-
indexedContent: 0,
|
|
1943
|
-
computedScopes: 0,
|
|
1944
|
-
linked: 0,
|
|
1945
|
-
indexedReferences: 0,
|
|
1946
|
-
validated: 0,
|
|
1947
|
-
};
|
|
1948
|
-
for (const doc of docs) {
|
|
1949
|
-
const state = Number(doc?.state ?? -1);
|
|
1950
|
-
if (state >= DocumentState.Changed)
|
|
1951
|
-
summary.changed += 1;
|
|
1952
|
-
if (state >= DocumentState.Parsed)
|
|
1953
|
-
summary.parsed += 1;
|
|
1954
|
-
if (state >= DocumentState.IndexedContent)
|
|
1955
|
-
summary.indexedContent += 1;
|
|
1956
|
-
if (state >= DocumentState.ComputedScopes)
|
|
1957
|
-
summary.computedScopes += 1;
|
|
1958
|
-
if (state >= DocumentState.Linked)
|
|
1959
|
-
summary.linked += 1;
|
|
1960
|
-
if (state >= DocumentState.IndexedReferences)
|
|
1961
|
-
summary.indexedReferences += 1;
|
|
1962
|
-
if (state >= DocumentState.Validated)
|
|
1963
|
-
summary.validated += 1;
|
|
1964
|
-
}
|
|
1965
|
-
return summary;
|
|
1966
|
-
}
|
|
1967
|
-
async waitForValidatedWithTrace(reason, targetUris) {
|
|
1968
|
-
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
1969
|
-
if (!REST_DEBUG_ENABLED) {
|
|
1970
|
-
await builder.waitUntil(DocumentState.Validated);
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
const startedAt = Date.now();
|
|
1974
|
-
const seenValidated = new Set();
|
|
1975
|
-
const beforeDocs = this.getWorkspaceOmlDocuments();
|
|
1976
|
-
const beforeStates = this.summarizeDocumentStates(beforeDocs);
|
|
1977
|
-
const beforeByUri = new Map(beforeDocs.map((doc) => [String(doc?.uri ?? '').trim(), Number(doc?.state ?? -1)]));
|
|
1978
|
-
const targetList = targetUris
|
|
1979
|
-
? [...targetUris].filter((uri) => uri.toLowerCase().endsWith('.oml'))
|
|
1980
|
-
: [...beforeByUri.keys()];
|
|
1981
|
-
const alreadyValidatedBeforeWait = targetList.filter((uri) => (beforeByUri.get(uri) ?? -1) >= DocumentState.Validated).length;
|
|
1982
|
-
const pendingBeforeWait = targetList.filter((uri) => (beforeByUri.get(uri) ?? -1) < DocumentState.Validated);
|
|
1983
|
-
const listener = builder.onDocumentPhase(DocumentState.Validated, (doc) => {
|
|
1984
|
-
const uri = String(doc?.uri ?? '').trim();
|
|
1985
|
-
if (!uri) {
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
if (targetUris && !targetUris.has(uri)) {
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
if (seenValidated.has(uri)) {
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
seenValidated.add(uri);
|
|
1995
|
-
const count = seenValidated.size;
|
|
1996
|
-
if (count <= 5 || count % 25 === 0) {
|
|
1997
|
-
debugRest('workspace.validated.doc', { reason, count, uri });
|
|
1998
|
-
}
|
|
1999
|
-
});
|
|
2000
|
-
try {
|
|
2001
|
-
debugRest('workspace.validated.wait.begin', {
|
|
2002
|
-
reason,
|
|
2003
|
-
targetCount: targetUris ? targetUris.size : -1,
|
|
2004
|
-
docs: beforeDocs.length,
|
|
2005
|
-
stateChanged: beforeStates.changed,
|
|
2006
|
-
stateParsed: beforeStates.parsed,
|
|
2007
|
-
stateIndexedContent: beforeStates.indexedContent,
|
|
2008
|
-
stateComputedScopes: beforeStates.computedScopes,
|
|
2009
|
-
stateLinked: beforeStates.linked,
|
|
2010
|
-
stateIndexedReferences: beforeStates.indexedReferences,
|
|
2011
|
-
stateValidated: beforeStates.validated,
|
|
2012
|
-
alreadyValidatedBeforeWait,
|
|
2013
|
-
pendingBeforeWait: pendingBeforeWait.length,
|
|
2014
|
-
pendingBeforeWaitSample: pendingBeforeWait.slice(0, 8).join(' || '),
|
|
2015
|
-
});
|
|
2016
|
-
await builder.waitUntil(DocumentState.Validated);
|
|
2017
|
-
const afterDocs = this.getWorkspaceOmlDocuments();
|
|
2018
|
-
const afterStates = this.summarizeDocumentStates(afterDocs);
|
|
2019
|
-
const afterByUri = new Map(afterDocs.map((doc) => [String(doc?.uri ?? '').trim(), Number(doc?.state ?? -1)]));
|
|
2020
|
-
const pendingAfterWait = targetList.filter((uri) => (afterByUri.get(uri) ?? -1) < DocumentState.Validated);
|
|
2021
|
-
debugRest('workspace.validated.wait.end', {
|
|
2022
|
-
reason,
|
|
2023
|
-
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
2024
|
-
observedValidatedDocs: seenValidated.size,
|
|
2025
|
-
docs: afterDocs.length,
|
|
2026
|
-
stateChanged: afterStates.changed,
|
|
2027
|
-
stateParsed: afterStates.parsed,
|
|
2028
|
-
stateIndexedContent: afterStates.indexedContent,
|
|
2029
|
-
stateComputedScopes: afterStates.computedScopes,
|
|
2030
|
-
stateLinked: afterStates.linked,
|
|
2031
|
-
stateIndexedReferences: afterStates.indexedReferences,
|
|
2032
|
-
stateValidated: afterStates.validated,
|
|
2033
|
-
pendingAfterWait: pendingAfterWait.length,
|
|
2034
|
-
pendingAfterWaitSample: pendingAfterWait.slice(0, 8).join(' || '),
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
finally {
|
|
2038
|
-
listener.dispose();
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
async lintWorkspace(_params = {}) {
|
|
2042
|
-
const preDocs = this.getWorkspaceOmlDocuments();
|
|
2043
|
-
const preSummary = this.summarizeDiagnostics(preDocs);
|
|
2044
|
-
const preStates = this.summarizeDocumentStates(preDocs);
|
|
2045
|
-
debugRest('lint.begin', {
|
|
2046
|
-
workspaceRoot: path.resolve(this.workspaceRoot),
|
|
2047
|
-
docs: preSummary.docs,
|
|
2048
|
-
diagnostics: preSummary.diagnostics,
|
|
2049
|
-
errors: preSummary.errors,
|
|
2050
|
-
warnings: preSummary.warnings,
|
|
2051
|
-
unresolved: preSummary.unresolved,
|
|
2052
|
-
unresolvedSample: preSummary.unresolvedSamples.join(' || '),
|
|
2053
|
-
stateChanged: preStates.changed,
|
|
2054
|
-
stateParsed: preStates.parsed,
|
|
2055
|
-
stateIndexedContent: preStates.indexedContent,
|
|
2056
|
-
stateComputedScopes: preStates.computedScopes,
|
|
2057
|
-
stateLinked: preStates.linked,
|
|
2058
|
-
stateIndexedReferences: preStates.indexedReferences,
|
|
2059
|
-
stateValidated: preStates.validated,
|
|
2060
|
-
});
|
|
2127
|
+
async lintWorkspace(params = {}) {
|
|
2061
2128
|
const context = {
|
|
2062
2129
|
workspaceRoot: this.workspaceRoot,
|
|
2063
2130
|
runtime: this.runtime,
|
|
2064
2131
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2065
|
-
ensureWorkspaceCurrent: () => this.
|
|
2132
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2133
|
+
waitForValidated: () => this.waitForValidatedWithTrace(),
|
|
2066
2134
|
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
2067
2135
|
};
|
|
2068
|
-
const result = await lintWorkspace(context);
|
|
2069
|
-
const postDocs = this.getWorkspaceOmlDocuments();
|
|
2070
|
-
const postStates = this.summarizeDocumentStates(postDocs);
|
|
2071
|
-
debugRest('lint.end', {
|
|
2072
|
-
filesChecked: result.filesChecked,
|
|
2073
|
-
errors: result.errors,
|
|
2074
|
-
warnings: result.warnings,
|
|
2075
|
-
problems: result.problems.length,
|
|
2076
|
-
elapsedMs: result.elapsedMs,
|
|
2077
|
-
stateValidated: postStates.validated,
|
|
2078
|
-
stateLinked: postStates.linked,
|
|
2079
|
-
stateComputedScopes: postStates.computedScopes,
|
|
2080
|
-
});
|
|
2136
|
+
const result = await lintWorkspace(context, params);
|
|
2081
2137
|
return result;
|
|
2082
2138
|
}
|
|
2083
2139
|
async queryWorkspaceRaw(modelUri, sparql) {
|
|
2084
2140
|
await this.ensureInitialized();
|
|
2085
|
-
await this.
|
|
2141
|
+
await this.ensureWorkspaceSettled(false);
|
|
2086
2142
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2087
2143
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2088
2144
|
return await reasoningService.getSparqlService().query(modelUri, sparql);
|
|
2089
2145
|
}
|
|
2090
2146
|
async askWorkspaceRaw(modelUri, sparql) {
|
|
2091
2147
|
await this.ensureInitialized();
|
|
2092
|
-
await this.
|
|
2148
|
+
await this.ensureWorkspaceSettled(false);
|
|
2093
2149
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2094
2150
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2095
2151
|
return await reasoningService.getSparqlService().ask(modelUri, sparql);
|
|
2096
2152
|
}
|
|
2097
2153
|
async constructWorkspaceRaw(modelUri, sparql) {
|
|
2098
2154
|
await this.ensureInitialized();
|
|
2099
|
-
await this.
|
|
2155
|
+
await this.ensureWorkspaceSettled(false);
|
|
2100
2156
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2101
2157
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2102
2158
|
return await reasoningService.getSparqlService().construct(modelUri, sparql);
|
|
@@ -2167,7 +2223,8 @@ class InMemoryJsonRpcLspClient {
|
|
|
2167
2223
|
}
|
|
2168
2224
|
async updateWorkspace(params) {
|
|
2169
2225
|
await this.ensureInitialized();
|
|
2170
|
-
await this.
|
|
2226
|
+
await this.ensureWorkspaceSettled(false);
|
|
2227
|
+
const preview = params.preview === true;
|
|
2171
2228
|
const lint = await this.lintWorkspace({});
|
|
2172
2229
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2173
2230
|
return {
|
|
@@ -2178,11 +2235,125 @@ class InMemoryJsonRpcLspClient {
|
|
|
2178
2235
|
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
|
|
2179
2236
|
};
|
|
2180
2237
|
}
|
|
2181
|
-
|
|
2238
|
+
const result = await applyOmlUpdate(this.runtime.shared, normalizeUpdateRequest(params), (message) => this.logError(message), pathToFileURL(this.workspaceRoot).toString(), preview);
|
|
2239
|
+
const errors = Array.isArray(result?.errors) ? result.errors : [];
|
|
2240
|
+
if (errors.length > 0) {
|
|
2241
|
+
return result;
|
|
2242
|
+
}
|
|
2243
|
+
const edit = result?.edit;
|
|
2244
|
+
const changedFiles = [];
|
|
2245
|
+
if (edit) {
|
|
2246
|
+
if (preview) {
|
|
2247
|
+
changedFiles.push(...collectWorkspaceEditChangedFiles(edit));
|
|
2248
|
+
const changedUris = collectWorkspaceEditReloadUris(edit);
|
|
2249
|
+
if (changedUris.length > 0) {
|
|
2250
|
+
await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, []);
|
|
2251
|
+
await this.waitForValidatedWithTrace();
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
else {
|
|
2255
|
+
try {
|
|
2256
|
+
const deletedUriStrings = new Set((edit.documentChanges ?? [])
|
|
2257
|
+
.filter((change) => change?.kind === 'delete' && typeof change?.uri === 'string')
|
|
2258
|
+
.map((change) => String(change.uri)));
|
|
2259
|
+
changedFiles.push(...await applyWorkspaceEditToFiles(edit));
|
|
2260
|
+
if (changedFiles.length > 0) {
|
|
2261
|
+
const changedUris = changedFiles
|
|
2262
|
+
.filter((uri) => !deletedUriStrings.has(uri))
|
|
2263
|
+
.map((uri) => URI.parse(uri));
|
|
2264
|
+
const deletedUris = [...deletedUriStrings].map((uri) => URI.parse(uri));
|
|
2265
|
+
await this.runtime.shared.workspace.DocumentBuilder.update(changedUris, deletedUris);
|
|
2266
|
+
await this.waitForValidatedWithTrace();
|
|
2267
|
+
if (changedUris.length > 0) {
|
|
2268
|
+
await this.forceRevalidateUris(changedUris);
|
|
2269
|
+
}
|
|
2270
|
+
// Force one canonical workspace refresh cycle so diagnostics are computed
|
|
2271
|
+
// from the latest on-disk snapshot, not a transient intermediate version.
|
|
2272
|
+
await this.refreshWorkspaceOmlDocuments();
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
catch (error) {
|
|
2276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2277
|
+
return {
|
|
2278
|
+
...result,
|
|
2279
|
+
errors: [{ operationIndex: -1, message }],
|
|
2280
|
+
error: message,
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (preview) {
|
|
2286
|
+
const diagnostics = await this.lintWorkspaceStable({});
|
|
2287
|
+
return {
|
|
2288
|
+
...result,
|
|
2289
|
+
changedFiles,
|
|
2290
|
+
diagnostics: {
|
|
2291
|
+
filesChecked: diagnostics.filesChecked,
|
|
2292
|
+
errors: diagnostics.errors,
|
|
2293
|
+
warnings: diagnostics.warnings,
|
|
2294
|
+
problems: diagnostics.problems,
|
|
2295
|
+
},
|
|
2296
|
+
success: true,
|
|
2297
|
+
preview: true,
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
const diagnostics = await this.lintWorkspaceStable({});
|
|
2301
|
+
return {
|
|
2302
|
+
...result,
|
|
2303
|
+
changedFiles,
|
|
2304
|
+
diagnostics: {
|
|
2305
|
+
filesChecked: diagnostics.filesChecked,
|
|
2306
|
+
errors: diagnostics.errors,
|
|
2307
|
+
warnings: diagnostics.warnings,
|
|
2308
|
+
problems: diagnostics.problems,
|
|
2309
|
+
},
|
|
2310
|
+
success: true,
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
lintFingerprint(lint) {
|
|
2314
|
+
const problems = lint.problems
|
|
2315
|
+
.map((problem) => [
|
|
2316
|
+
problem.uri,
|
|
2317
|
+
problem.line,
|
|
2318
|
+
problem.column,
|
|
2319
|
+
problem.severity,
|
|
2320
|
+
problem.kind,
|
|
2321
|
+
problem.source ?? '',
|
|
2322
|
+
problem.code ?? '',
|
|
2323
|
+
problem.message,
|
|
2324
|
+
].join('|'))
|
|
2325
|
+
.join('\n');
|
|
2326
|
+
return `${lint.filesChecked}|${lint.errors}|${lint.warnings}\n${problems}`;
|
|
2327
|
+
}
|
|
2328
|
+
async lintWorkspaceStable(params) {
|
|
2329
|
+
let previous;
|
|
2330
|
+
let previousFingerprint = '';
|
|
2331
|
+
for (let i = 0; i < 6; i += 1) {
|
|
2332
|
+
await this.waitForWatcherRefreshIdle();
|
|
2333
|
+
await this.waitForValidatedWithTrace();
|
|
2334
|
+
const current = await this.lintWorkspace(params);
|
|
2335
|
+
const currentFingerprint = this.lintFingerprint(current);
|
|
2336
|
+
if (previous && previousFingerprint === currentFingerprint) {
|
|
2337
|
+
return current;
|
|
2338
|
+
}
|
|
2339
|
+
previous = current;
|
|
2340
|
+
previousFingerprint = currentFingerprint;
|
|
2341
|
+
}
|
|
2342
|
+
return previous ?? await this.lintWorkspace(params);
|
|
2343
|
+
}
|
|
2344
|
+
async forceRevalidateUris(changedUris) {
|
|
2345
|
+
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
2346
|
+
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2347
|
+
const docs = await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
|
|
2348
|
+
if (docs.length === 0) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
await builder.build(docs, { validation: true });
|
|
2352
|
+
await this.waitForValidatedWithTrace();
|
|
2182
2353
|
}
|
|
2183
2354
|
async fuzzySearchWorkspace(params) {
|
|
2184
2355
|
await this.ensureInitialized();
|
|
2185
|
-
await this.
|
|
2356
|
+
await this.ensureWorkspaceSettled(false);
|
|
2186
2357
|
try {
|
|
2187
2358
|
const text = (typeof params.text === 'string' ? params.text : '').trim();
|
|
2188
2359
|
if (!text) {
|
|
@@ -2199,7 +2370,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
2199
2370
|
const candidateByIri = new Map();
|
|
2200
2371
|
for (const doc of iterable) {
|
|
2201
2372
|
const root = doc?.parseResult?.value;
|
|
2202
|
-
|
|
2373
|
+
const ontologyOk = !!root && isOntology(root);
|
|
2374
|
+
const vocabOrDescOk = ontologyOk && (isVocabulary(root) || isDescription(root));
|
|
2375
|
+
if (!vocabOrDescOk) {
|
|
2203
2376
|
continue;
|
|
2204
2377
|
}
|
|
2205
2378
|
modelUris.push(doc.uri.toString());
|
|
@@ -2254,7 +2427,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2254
2427
|
}
|
|
2255
2428
|
}
|
|
2256
2429
|
return {
|
|
2257
|
-
|
|
2430
|
+
member: entry.iri,
|
|
2258
2431
|
label: entry.label,
|
|
2259
2432
|
score: ranked.length - index,
|
|
2260
2433
|
diagnostics: {
|
|
@@ -2281,7 +2454,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2281
2454
|
}
|
|
2282
2455
|
async assertionsWorkspace(params) {
|
|
2283
2456
|
await this.ensureInitialized();
|
|
2284
|
-
await this.
|
|
2457
|
+
await this.ensureWorkspaceSettled(false);
|
|
2285
2458
|
const lint = await this.lintWorkspace({});
|
|
2286
2459
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2287
2460
|
return {
|
|
@@ -2294,10 +2467,24 @@ class InMemoryJsonRpcLspClient {
|
|
|
2294
2467
|
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s).`,
|
|
2295
2468
|
};
|
|
2296
2469
|
}
|
|
2297
|
-
const
|
|
2470
|
+
const ontologyIriFilterRaw = typeof params.ontologyIri === 'string' ? params.ontologyIri.trim() : '';
|
|
2471
|
+
let modelUriFilter = '';
|
|
2472
|
+
if (ontologyIriFilterRaw.length > 0) {
|
|
2473
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
|
|
2474
|
+
const resolved = ontologyIndex.resolveModelUri(ontologyIriFilterRaw);
|
|
2475
|
+
if (!resolved) {
|
|
2476
|
+
return {
|
|
2477
|
+
success: false,
|
|
2478
|
+
files: [],
|
|
2479
|
+
error: `No model found for ontology IRI '${ontologyIriFilterRaw}'.`,
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
modelUriFilter = resolved;
|
|
2483
|
+
}
|
|
2298
2484
|
const format = normalizeRdfSerializationFormat(params.format);
|
|
2299
2485
|
const pretty = params.pretty === true;
|
|
2300
2486
|
const docs = this.getWorkspaceOmlDocuments();
|
|
2487
|
+
const workspacePrefixes = this.getWorkspacePrefixes();
|
|
2301
2488
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2302
2489
|
const store = reasoningService.getStore().getStore();
|
|
2303
2490
|
const files = [];
|
|
@@ -2317,8 +2504,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2317
2504
|
files.push({
|
|
2318
2505
|
modelUri,
|
|
2319
2506
|
ontologyIri,
|
|
2320
|
-
|
|
2321
|
-
content: await serializeQuads(quads, format, pretty),
|
|
2507
|
+
content: await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)),
|
|
2322
2508
|
});
|
|
2323
2509
|
}
|
|
2324
2510
|
files.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
|
|
@@ -2330,6 +2516,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2330
2516
|
async writeWorkspaceAssertedOwl(outputDir, format, pretty) {
|
|
2331
2517
|
const entries = [];
|
|
2332
2518
|
const docs = this.getWorkspaceOmlDocuments();
|
|
2519
|
+
const workspacePrefixes = this.getWorkspacePrefixes();
|
|
2333
2520
|
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2334
2521
|
const store = reasoningService.getStore().getStore();
|
|
2335
2522
|
for (const doc of docs) {
|
|
@@ -2344,7 +2531,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2344
2531
|
.map((quad) => DataFactory.quad(quad.subject, quad.predicate, quad.object));
|
|
2345
2532
|
const owlPath = path.join(outputDir, resolveOutputPathFromOntologyIriString(ontologyIri, format));
|
|
2346
2533
|
await fs.mkdir(path.dirname(owlPath), { recursive: true });
|
|
2347
|
-
await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty), 'utf-8');
|
|
2534
|
+
await fs.writeFile(owlPath, await serializeQuads(quads, format, pretty, filterPrefixesForQuads(quads, workspacePrefixes)), 'utf-8');
|
|
2348
2535
|
entries.push({ modelUri, ontologyIri, owlPath });
|
|
2349
2536
|
}
|
|
2350
2537
|
entries.sort((left, right) => left.ontologyIri.localeCompare(right.ontologyIri));
|
|
@@ -2355,7 +2542,8 @@ class InMemoryJsonRpcLspClient {
|
|
|
2355
2542
|
workspaceRoot: this.workspaceRoot,
|
|
2356
2543
|
runtime: this.runtime,
|
|
2357
2544
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2358
|
-
ensureWorkspaceCurrent: () => this.
|
|
2545
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(true),
|
|
2546
|
+
waitForValidated: () => this.waitForValidatedWithTrace(),
|
|
2359
2547
|
getWorkspaceOmlDocuments: () => this.getWorkspaceOmlDocuments(),
|
|
2360
2548
|
};
|
|
2361
2549
|
return await validateWorkspace(context, params);
|
|
@@ -2375,7 +2563,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2375
2563
|
const context = {
|
|
2376
2564
|
workspaceRoot: this.workspaceRoot,
|
|
2377
2565
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2378
|
-
ensureWorkspaceCurrent: () => this.
|
|
2566
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2379
2567
|
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
2380
2568
|
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
2381
2569
|
};
|
|
@@ -2485,7 +2673,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2485
2673
|
const context = {
|
|
2486
2674
|
workspaceRoot: this.workspaceRoot,
|
|
2487
2675
|
ensureInitialized: () => this.ensureInitialized(),
|
|
2488
|
-
ensureWorkspaceCurrent: () => this.
|
|
2676
|
+
ensureWorkspaceCurrent: () => this.ensureWorkspaceSettled(false),
|
|
2489
2677
|
writeWorkspaceAssertedOwl: (outputDir, format, pretty) => this.writeWorkspaceAssertedOwl(outputDir, format, pretty),
|
|
2490
2678
|
exportAssertedWorkspace: (options) => exportAssertedWorkspace(context, options),
|
|
2491
2679
|
};
|
|
@@ -2493,7 +2681,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2493
2681
|
}
|
|
2494
2682
|
async renderWorkspace(params) {
|
|
2495
2683
|
await this.ensureInitialized();
|
|
2496
|
-
await this.
|
|
2684
|
+
await this.ensureWorkspaceSettled(false);
|
|
2497
2685
|
const lint = await this.lintWorkspace({});
|
|
2498
2686
|
if (lint.errors > 0 || lint.warnings > 0) {
|
|
2499
2687
|
return {
|
|
@@ -2511,9 +2699,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2511
2699
|
const outputFromOptions = typeof params.web === 'string' && params.web.trim().length > 0 ? params.web.trim() : 'build/web';
|
|
2512
2700
|
const inputDir = path.resolve(workspaceRoot, inputFromOptions);
|
|
2513
2701
|
const outputDir = path.resolve(workspaceRoot, outputFromOptions);
|
|
2514
|
-
|
|
2515
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
2516
|
-
}
|
|
2702
|
+
await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
2517
2703
|
const runtime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
|
|
2518
2704
|
const templateCatalog = await buildTemplateCatalog(workspaceRoot);
|
|
2519
2705
|
const navigationTemplateCatalog = buildNavigationTemplateCatalog(Array.from(templateCatalog.values()).flatMap((entries) => entries.map((entry) => entry.definition)));
|
|
@@ -2525,7 +2711,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
2525
2711
|
: undefined;
|
|
2526
2712
|
const defaultContextOntologyIri = contextOption
|
|
2527
2713
|
? (() => {
|
|
2528
|
-
const contextModelPath =
|
|
2714
|
+
const contextModelPath = contextOption.startsWith('file://')
|
|
2715
|
+
? fileURLToPath(contextOption)
|
|
2716
|
+
: path.resolve(workspaceRoot, contextOption);
|
|
2529
2717
|
const contextModelUri = pathToFileURL(contextModelPath).toString();
|
|
2530
2718
|
return ontologyIndex.resolveOntologyIri(contextModelUri);
|
|
2531
2719
|
})()
|
|
@@ -2558,7 +2746,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2558
2746
|
continue;
|
|
2559
2747
|
}
|
|
2560
2748
|
const frontMatter = extractLeadingFrontMatter(markdown);
|
|
2561
|
-
const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.
|
|
2749
|
+
const contextOntologyIri = normalizeContextOntologyIri(frontMatter?.ontology
|
|
2562
2750
|
?? frontMatterString(frontMatter?.data, 'ontologyIri')
|
|
2563
2751
|
?? frontMatterString(frontMatter?.data, 'ontology'));
|
|
2564
2752
|
const contextMemberIri = frontMatterString(frontMatter?.data, 'memberIri')
|
|
@@ -2624,6 +2812,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
2624
2812
|
filesRendered += 1;
|
|
2625
2813
|
}
|
|
2626
2814
|
const renderedNavigationPages = new Set();
|
|
2815
|
+
const navTasks = [];
|
|
2627
2816
|
for (const modelUri of contextModelUris) {
|
|
2628
2817
|
await reasoningService.ensureQueryContext(modelUri);
|
|
2629
2818
|
const membersToTypes = await queryMemberTypes(reasoningService, modelUri);
|
|
@@ -2672,55 +2861,60 @@ class InMemoryJsonRpcLspClient {
|
|
|
2672
2861
|
const syntheticSourcePath = resolution.template.sourceUri?.startsWith('file:')
|
|
2673
2862
|
? decodeURIComponent(new URL(resolution.template.sourceUri).pathname)
|
|
2674
2863
|
: path.join(workspaceRoot, 'index.md');
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2864
|
+
navTasks.push(async () => {
|
|
2865
|
+
const expanded = await expandTemplateComposeBlocks(rendered.output, templateCatalog, {
|
|
2866
|
+
workspaceRoot,
|
|
2867
|
+
sourceMarkdownPath: syntheticSourcePath,
|
|
2868
|
+
contextOntologyIri,
|
|
2869
|
+
contextModelUri: modelUri,
|
|
2870
|
+
contextMemberIri: memberIri,
|
|
2871
|
+
});
|
|
2872
|
+
const prepared = runtime.prepare(expanded);
|
|
2873
|
+
const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
|
|
2874
|
+
workspaceRoot,
|
|
2875
|
+
inputRoot: inputDir,
|
|
2876
|
+
inputFile: syntheticSourcePath,
|
|
2877
|
+
outputRoot: outputDir,
|
|
2878
|
+
outputFile: targetFile,
|
|
2879
|
+
});
|
|
2880
|
+
for (const asset of rewriteResult.workspaceAssets) {
|
|
2881
|
+
workspaceAssetFiles.add(asset);
|
|
2882
|
+
}
|
|
2883
|
+
const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
|
|
2884
|
+
const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options]));
|
|
2885
|
+
const blockResults = executableBlocks.length === 0
|
|
2886
|
+
? []
|
|
2887
|
+
: (await executor.executeBlocks({
|
|
2888
|
+
markdownUri: sourceDocumentUri,
|
|
2889
|
+
modelUri,
|
|
2890
|
+
blocks: executableBlocks,
|
|
2891
|
+
})).results.map((result) => ({
|
|
2892
|
+
...result,
|
|
2893
|
+
options: optionsByBlockId.get(result.blockId),
|
|
2894
|
+
}));
|
|
2895
|
+
const rewrittenBlockResults = blockResults.map((result) => rewriteBlockResultAssetPaths(result, {
|
|
2896
|
+
workspaceRoot,
|
|
2897
|
+
inputRoot: inputDir,
|
|
2898
|
+
sourceFile: syntheticSourcePath,
|
|
2899
|
+
outputRoot: outputDir,
|
|
2900
|
+
outputFile: targetFile,
|
|
2901
|
+
}, workspaceAssetFiles));
|
|
2902
|
+
const blockArtifacts = await writeBlockArtifacts(targetFile, rewrittenBlockResults);
|
|
2903
|
+
blockArtifactFiles += blockArtifacts.count;
|
|
2904
|
+
renderJobs.push({
|
|
2905
|
+
htmlPath: targetFile,
|
|
2906
|
+
renderedHtml: rewriteResult.html,
|
|
2907
|
+
blockManifest: blockArtifacts.manifest,
|
|
2908
|
+
blockResults: rewrittenBlockResults,
|
|
2909
|
+
wikiLinkHrefByKey: buildWikiLinkHrefMapForPage(wikiPageIndex, targetFile),
|
|
2910
|
+
});
|
|
2911
|
+
filesRendered += 1;
|
|
2720
2912
|
});
|
|
2721
|
-
filesRendered += 1;
|
|
2722
2913
|
}
|
|
2723
2914
|
}
|
|
2915
|
+
for (const task of navTasks) {
|
|
2916
|
+
await task();
|
|
2917
|
+
}
|
|
2724
2918
|
for (const job of renderJobs) {
|
|
2725
2919
|
const runtimeScriptRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.runtimeScriptFile);
|
|
2726
2920
|
const stylesheetRelative = toRelativeWebPath(path.dirname(job.htmlPath), staticAssets.stylesheetFile);
|
|
@@ -2754,35 +2948,145 @@ class InMemoryJsonRpcLspClient {
|
|
|
2754
2948
|
}
|
|
2755
2949
|
return { success: true, filesRendered, outputDir, blockArtifactFiles };
|
|
2756
2950
|
}
|
|
2757
|
-
async
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2951
|
+
async ontologiesWorkspace() {
|
|
2952
|
+
await this.ensureInitialized();
|
|
2953
|
+
await this.ensureWorkspaceSettled(false);
|
|
2954
|
+
return getOntologiesFromRuntime(this.runtime);
|
|
2955
|
+
}
|
|
2956
|
+
resolveModelUriForSparqlModelId(modelId) {
|
|
2957
|
+
const ontologyIndex = getOntologyModelIndex(this.runtime.shared);
|
|
2958
|
+
const docs = this.getWorkspaceOmlDocuments();
|
|
2959
|
+
return resolveModelUriFromIriModelId(docs, ontologyIndex, modelId);
|
|
2960
|
+
}
|
|
2961
|
+
async shapesWorkspace(params) {
|
|
2962
|
+
await this.ensureInitialized();
|
|
2963
|
+
await this.ensureWorkspaceSettled(true);
|
|
2964
|
+
const typeIri = typeof params.type === 'string' ? params.type.trim() : '';
|
|
2965
|
+
if (!typeIri) {
|
|
2966
|
+
return {
|
|
2967
|
+
success: false,
|
|
2968
|
+
error: 'Missing required parameter: type',
|
|
2969
|
+
};
|
|
2766
2970
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2971
|
+
try {
|
|
2972
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
2973
|
+
const reasoningStoreWrapper = reasoningService.getStore();
|
|
2974
|
+
const store = reasoningStoreWrapper.getStore();
|
|
2975
|
+
const OML_CONTEXT_ONTOLOGY = 'http://opencaesar.io/oml#contextOntology';
|
|
2976
|
+
const SH_NS = 'http://www.w3.org/ns/shacl#';
|
|
2977
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
2978
|
+
const shapeEntries = [];
|
|
2979
|
+
// Find all markdown graph URIs
|
|
2980
|
+
const markdownGraphs = new Set();
|
|
2981
|
+
const contextQuads = store.getQuads(null, DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null);
|
|
2982
|
+
for (const quad of contextQuads) {
|
|
2983
|
+
if (quad.subject.termType === 'NamedNode' && quad.object.termType === 'NamedNode') {
|
|
2984
|
+
markdownGraphs.add(quad.subject.value);
|
|
2985
|
+
}
|
|
2772
2986
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2987
|
+
// Query each markdown graph for shapes matching the type
|
|
2988
|
+
for (const graphUri of markdownGraphs) {
|
|
2989
|
+
const graphNode = DataFactory.namedNode(graphUri);
|
|
2990
|
+
const ontologyQuad = store.getQuads(DataFactory.namedNode(graphUri), DataFactory.namedNode(OML_CONTEXT_ONTOLOGY), null, null)[0];
|
|
2991
|
+
if (!ontologyQuad || ontologyQuad.object.termType !== 'NamedNode') {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
const ontologyIri = ontologyQuad.object.value;
|
|
2995
|
+
// Find NodeShapes in this graph
|
|
2996
|
+
const nodeShapeQuads = store.getQuads(null, DataFactory.namedNode(RDF_TYPE), DataFactory.namedNode(`${SH_NS}NodeShape`), graphNode);
|
|
2997
|
+
for (const shapeQuad of nodeShapeQuads) {
|
|
2998
|
+
if (shapeQuad.subject.termType !== 'NamedNode') {
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
const shapeIri = shapeQuad.subject.value;
|
|
3002
|
+
// Check if this shape targets our type (sh:targetClass)
|
|
3003
|
+
const targetClassQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}targetClass`), DataFactory.namedNode(typeIri), graphNode);
|
|
3004
|
+
if (targetClassQuads.length > 0) {
|
|
3005
|
+
shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
// Check if any property shape has sh:class matching our type
|
|
3009
|
+
const propertyQuads = store.getQuads(shapeQuad.subject, DataFactory.namedNode(`${SH_NS}property`), null, graphNode);
|
|
3010
|
+
for (const propQuad of propertyQuads) {
|
|
3011
|
+
if (propQuad.object.termType === 'NamedNode' || propQuad.object.termType === 'BlankNode') {
|
|
3012
|
+
const classQuads = store.getQuads(propQuad.object, DataFactory.namedNode(`${SH_NS}class`), DataFactory.namedNode(typeIri), graphNode);
|
|
3013
|
+
if (classQuads.length > 0) {
|
|
3014
|
+
shapeEntries.push({ ontology: ontologyIri, shape: shapeIri, graphUri });
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
2775
3020
|
}
|
|
2776
|
-
|
|
2777
|
-
|
|
3021
|
+
// Helper to collect all axioms for a shape (including blank nodes)
|
|
3022
|
+
const collectShapeAxioms = (shapeIri, graphUri) => {
|
|
3023
|
+
const graphNode = DataFactory.namedNode(graphUri);
|
|
3024
|
+
const visited = new Set();
|
|
3025
|
+
const axioms = [];
|
|
3026
|
+
const writer = new Writer({ format: 'N-Triples' });
|
|
3027
|
+
const collectNode = (node) => {
|
|
3028
|
+
const nodeKey = node.termType === 'BlankNode' ? `_:${node.value}` : node.value;
|
|
3029
|
+
if (visited.has(nodeKey))
|
|
3030
|
+
return;
|
|
3031
|
+
visited.add(nodeKey);
|
|
3032
|
+
const quads = store.getQuads(node, null, null, graphNode);
|
|
3033
|
+
for (const quad of quads) {
|
|
3034
|
+
// Convert quad to triple (without graph)
|
|
3035
|
+
writer.addQuad(DataFactory.quad(quad.subject, quad.predicate, quad.object));
|
|
3036
|
+
// If object is a blank node, recursively collect its triples
|
|
3037
|
+
if (quad.object.termType === 'BlankNode') {
|
|
3038
|
+
collectNode(quad.object);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
collectNode(DataFactory.namedNode(shapeIri));
|
|
3043
|
+
// Get serialized triples
|
|
3044
|
+
writer.end((error, result) => {
|
|
3045
|
+
if (!error && result) {
|
|
3046
|
+
const lines = result.trim().split('\n').filter((line) => line.trim().length > 0);
|
|
3047
|
+
axioms.push(...lines);
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
return axioms;
|
|
3051
|
+
};
|
|
3052
|
+
// Group by ontology and collect axioms
|
|
3053
|
+
const grouped = new Map();
|
|
3054
|
+
for (const entry of shapeEntries) {
|
|
3055
|
+
const axioms = collectShapeAxioms(entry.shape, entry.graphUri);
|
|
3056
|
+
const shapes = grouped.get(entry.ontology) ?? [];
|
|
3057
|
+
shapes.push({ shape: entry.shape, axioms });
|
|
3058
|
+
grouped.set(entry.ontology, shapes);
|
|
3059
|
+
}
|
|
3060
|
+
const results = Array.from(grouped.entries())
|
|
3061
|
+
.map(([ontology, shapes]) => ({
|
|
3062
|
+
ontology,
|
|
3063
|
+
shapes: shapes.sort((a, b) => a.shape.localeCompare(b.shape)),
|
|
3064
|
+
}))
|
|
3065
|
+
.sort((a, b) => a.ontology.localeCompare(b.ontology));
|
|
3066
|
+
return {
|
|
3067
|
+
success: true,
|
|
3068
|
+
results,
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
catch (error) {
|
|
3072
|
+
return {
|
|
3073
|
+
success: false,
|
|
3074
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3075
|
+
};
|
|
2778
3076
|
}
|
|
3077
|
+
}
|
|
3078
|
+
async ensureWorkspaceSettled(includeShacl) {
|
|
2779
3079
|
if (!this.initialWorkspaceSyncCompleted) {
|
|
2780
3080
|
await this.bootstrapWorkspaceOmlDocuments();
|
|
2781
3081
|
this.initialWorkspaceSyncCompleted = true;
|
|
2782
|
-
debugRest('workspace.current.initial.bootstrap.complete');
|
|
2783
3082
|
}
|
|
2784
3083
|
await this.waitForWatcherRefreshIdle();
|
|
2785
|
-
|
|
3084
|
+
if (this.useExternalRuntime) {
|
|
3085
|
+
await this.waitForValidatedWithTrace();
|
|
3086
|
+
}
|
|
3087
|
+
if (includeShacl) {
|
|
3088
|
+
await this.reindexWorkspaceShacl();
|
|
3089
|
+
}
|
|
2786
3090
|
}
|
|
2787
3091
|
async waitForWatcherRefreshIdle() {
|
|
2788
3092
|
while (this.watcherFlushTimer !== undefined || this.refreshInFlight || this.refreshQueued) {
|
|
@@ -2810,18 +3114,10 @@ class InMemoryJsonRpcLspClient {
|
|
|
2810
3114
|
const deletedUris = [...currentOmlUris]
|
|
2811
3115
|
.filter((uri) => !changedUriStrings.has(uri))
|
|
2812
3116
|
.map((uri) => URI.parse(uri));
|
|
2813
|
-
debugRest('workspace.refresh.plan', {
|
|
2814
|
-
workspaceRoot,
|
|
2815
|
-
changed: changedUris.length,
|
|
2816
|
-
deleted: deletedUris.length,
|
|
2817
|
-
current: currentOmlUris.size,
|
|
2818
|
-
});
|
|
2819
3117
|
await Promise.all(changedUris.map((uri) => documents.getOrCreateDocument(uri)));
|
|
2820
3118
|
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2821
3119
|
await builder.update(changedUris, deletedUris);
|
|
2822
|
-
await this.waitForValidatedWithTrace(
|
|
2823
|
-
const postDocs = this.getWorkspaceOmlDocuments();
|
|
2824
|
-
debugRest('workspace.refresh.done', { docsAfter: postDocs.length });
|
|
3120
|
+
await this.waitForValidatedWithTrace();
|
|
2825
3121
|
}
|
|
2826
3122
|
async bootstrapWorkspaceOmlDocuments() {
|
|
2827
3123
|
const workspaceRoot = path.resolve(this.workspaceRoot);
|
|
@@ -2829,38 +3125,32 @@ class InMemoryJsonRpcLspClient {
|
|
|
2829
3125
|
const documents = this.runtime.shared.workspace.LangiumDocuments;
|
|
2830
3126
|
const builder = this.runtime.shared.workspace.DocumentBuilder;
|
|
2831
3127
|
const docs = await Promise.all(omlFiles.map(async (filePath) => documents.getOrCreateDocument(URI.parse(pathToFileURL(filePath).toString()))));
|
|
2832
|
-
debugRest('workspace.bootstrap.plan', {
|
|
2833
|
-
workspaceRoot,
|
|
2834
|
-
files: docs.length,
|
|
2835
|
-
});
|
|
2836
3128
|
if (docs.length > 0) {
|
|
2837
3129
|
await builder.build(docs, { validation: true });
|
|
2838
|
-
await this.waitForValidatedWithTrace(
|
|
3130
|
+
await this.waitForValidatedWithTrace();
|
|
2839
3131
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
3132
|
+
}
|
|
3133
|
+
async reindexWorkspaceShacl() {
|
|
3134
|
+
await reindexWorkspaceShacl({
|
|
3135
|
+
workspaceRoot: this.workspaceRoot,
|
|
3136
|
+
runtime: this.runtime,
|
|
3137
|
+
});
|
|
2842
3138
|
}
|
|
2843
3139
|
async ensureInitialized() {
|
|
2844
3140
|
if (this.initPromise) {
|
|
2845
|
-
debugRest('initialize.await.existing');
|
|
2846
3141
|
await this.initPromise;
|
|
2847
3142
|
return;
|
|
2848
3143
|
}
|
|
2849
3144
|
if (this.useExternalRuntime) {
|
|
2850
3145
|
this.initPromise = (async () => {
|
|
2851
|
-
const begin = Date.now();
|
|
2852
|
-
debugRest('initialize.external.begin');
|
|
2853
3146
|
const workspace = this.runtime.shared.workspace.WorkspaceManager;
|
|
2854
3147
|
await workspace.ready;
|
|
2855
|
-
debugRest('initialize.external.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
|
|
2856
3148
|
})();
|
|
2857
3149
|
}
|
|
2858
3150
|
else {
|
|
2859
3151
|
const rootPath = path.resolve(this.workspaceRoot);
|
|
2860
3152
|
const rootUri = pathToFileURL(rootPath).toString();
|
|
2861
3153
|
this.initPromise = (async () => {
|
|
2862
|
-
const begin = Date.now();
|
|
2863
|
-
debugRest('initialize.internal.begin', { rootPath, rootUri });
|
|
2864
3154
|
await withTimeout(this.clientConnection.sendRequest('initialize', {
|
|
2865
3155
|
processId: process.pid,
|
|
2866
3156
|
rootUri,
|
|
@@ -2874,14 +3164,13 @@ class InMemoryJsonRpcLspClient {
|
|
|
2874
3164
|
// before lint/query calls trigger document refresh and diagnostics.
|
|
2875
3165
|
const workspace = this.runtime.shared.workspace.WorkspaceManager;
|
|
2876
3166
|
await workspace.ready;
|
|
2877
|
-
debugRest('initialize.internal.ready', { elapsedMs: Math.max(0, Date.now() - begin) });
|
|
2878
3167
|
})();
|
|
2879
3168
|
}
|
|
2880
3169
|
await this.initPromise;
|
|
2881
3170
|
}
|
|
2882
3171
|
}
|
|
2883
3172
|
export async function runOmlWorkspaceLocalOperation(options) {
|
|
2884
|
-
const client = new InMemoryJsonRpcLspClient(options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd(), options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.
|
|
3173
|
+
const client = new InMemoryJsonRpcLspClient(options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd(), options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
|
|
2885
3174
|
try {
|
|
2886
3175
|
const params = options.params ?? {};
|
|
2887
3176
|
switch (options.operation) {
|
|
@@ -2905,16 +3194,7 @@ export async function runOmlWorkspaceLocalOperation(options) {
|
|
|
2905
3194
|
}
|
|
2906
3195
|
export async function startOmlRestServer(options) {
|
|
2907
3196
|
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2908
|
-
|
|
2909
|
-
host: options.host,
|
|
2910
|
-
requestedPort: options.port,
|
|
2911
|
-
workspaceRoot,
|
|
2912
|
-
watchWorkspace: options.watchWorkspace === true,
|
|
2913
|
-
hasRuntime: options.runtime !== undefined,
|
|
2914
|
-
hasAuthToken: Boolean(options.authToken),
|
|
2915
|
-
hasFeatureGate: Boolean(options.featureGate),
|
|
2916
|
-
});
|
|
2917
|
-
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.watchWorkspace === true, options.runtime);
|
|
3197
|
+
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.runtime);
|
|
2918
3198
|
let openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2919
3199
|
let currentAccessToken = options.authToken;
|
|
2920
3200
|
const featureGate = options.featureGate;
|
|
@@ -2927,17 +3207,6 @@ export async function startOmlRestServer(options) {
|
|
|
2927
3207
|
const requestId = randomUUID();
|
|
2928
3208
|
const method = req.method ?? 'GET';
|
|
2929
3209
|
const rawUrl = req.url ?? '/';
|
|
2930
|
-
const requestStartedAt = Date.now();
|
|
2931
|
-
debugRest('request.begin', { requestId, method, rawUrl });
|
|
2932
|
-
res.once('finish', () => {
|
|
2933
|
-
debugRest('request.end', {
|
|
2934
|
-
requestId,
|
|
2935
|
-
method,
|
|
2936
|
-
rawUrl,
|
|
2937
|
-
statusCode: res.statusCode,
|
|
2938
|
-
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
|
2939
|
-
});
|
|
2940
|
-
});
|
|
2941
3210
|
try {
|
|
2942
3211
|
const parsed = new URL(rawUrl, `http://${req.headers.host ?? 'localhost'}`);
|
|
2943
3212
|
const pathname = parsed.pathname;
|
|
@@ -2983,13 +3252,23 @@ export async function startOmlRestServer(options) {
|
|
|
2983
3252
|
await serveSwaggerAsset(res, assetName);
|
|
2984
3253
|
return;
|
|
2985
3254
|
}
|
|
2986
|
-
if (method === 'GET' && pathname === '/v0/
|
|
2987
|
-
const
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
3255
|
+
if (method === 'GET' && pathname === '/v0/workspace') {
|
|
3256
|
+
const ontologies = await client.ontologiesWorkspace();
|
|
3257
|
+
const allImported = new Set(ontologies.flatMap((o) => o.imports));
|
|
3258
|
+
const roots = ontologies
|
|
3259
|
+
.filter((o) => !allImported.has(o.ontology))
|
|
3260
|
+
.map((o) => o.ontology);
|
|
3261
|
+
jsonResponse(res, 200, {
|
|
3262
|
+
workspaceUri: pathToFileURL(workspaceRoot).toString(),
|
|
3263
|
+
roots,
|
|
3264
|
+
requestId,
|
|
3265
|
+
});
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
if (method === 'GET' && pathname === '/v0/ontologies') {
|
|
3269
|
+
const ontologies = await client.ontologiesWorkspace();
|
|
2991
3270
|
jsonResponse(res, 200, {
|
|
2992
|
-
|
|
3271
|
+
ontologies,
|
|
2993
3272
|
requestId,
|
|
2994
3273
|
});
|
|
2995
3274
|
return;
|
|
@@ -3000,13 +3279,7 @@ export async function startOmlRestServer(options) {
|
|
|
3000
3279
|
if (!routeModelPath) {
|
|
3001
3280
|
throw new RestHttpError(404, `No route for ${method} ${pathname}.`);
|
|
3002
3281
|
}
|
|
3003
|
-
const modelUri =
|
|
3004
|
-
try {
|
|
3005
|
-
await assertModelUriExists(modelUri);
|
|
3006
|
-
}
|
|
3007
|
-
catch {
|
|
3008
|
-
throw new RestHttpError(404, `No OML model found for '${routeModelPath}'.`);
|
|
3009
|
-
}
|
|
3282
|
+
const modelUri = client.resolveModelUriForSparqlModelId(routeModelPath);
|
|
3010
3283
|
const bodyText = method === 'POST' ? await readTextBody(req) : '';
|
|
3011
3284
|
const sparql = parseSparqlQueryFromRequest(req, parsed, bodyText);
|
|
3012
3285
|
if (!sparql) {
|
|
@@ -3034,7 +3307,7 @@ export async function startOmlRestServer(options) {
|
|
|
3034
3307
|
if (!graphFormat) {
|
|
3035
3308
|
throw new RestHttpError(406, 'Not Acceptable for graph results.');
|
|
3036
3309
|
}
|
|
3037
|
-
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false);
|
|
3310
|
+
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false, filterPrefixesForQuads(queryResult.result.quads, client.getWorkspacePrefixes()));
|
|
3038
3311
|
textResponse(res, 200, graphFormat.contentType, serialized);
|
|
3039
3312
|
};
|
|
3040
3313
|
const requiredFeature = requiredFeatureForRestOperation('query');
|
|
@@ -3067,7 +3340,6 @@ export async function startOmlRestServer(options) {
|
|
|
3067
3340
|
}
|
|
3068
3341
|
catch (error) {
|
|
3069
3342
|
const message = error instanceof Error ? error.message : String(error);
|
|
3070
|
-
debugRest('request.error', { requestId, method, rawUrl, message });
|
|
3071
3343
|
if (error instanceof OmlAccessError) {
|
|
3072
3344
|
jsonResponse(res, error.statusCode, accessErrorPayload(error, requestId));
|
|
3073
3345
|
return;
|
|
@@ -3090,18 +3362,15 @@ export async function startOmlRestServer(options) {
|
|
|
3090
3362
|
server.listen(options.port, options.host, () => resolve());
|
|
3091
3363
|
});
|
|
3092
3364
|
const listeningPort = resolveListeningPort(server);
|
|
3093
|
-
debugRest('rest.start.listening', { host: options.host, requestedPort: options.port, listeningPort });
|
|
3094
3365
|
openApiSpec = createOpenApiSpec(options.host, listeningPort);
|
|
3095
3366
|
return {
|
|
3096
3367
|
server,
|
|
3097
3368
|
updateToken: async (token) => {
|
|
3098
|
-
debugRest('token.update.begin', { tokenProvided: Boolean(token) });
|
|
3099
3369
|
currentAccessToken = token;
|
|
3100
3370
|
featureGate?.setAccessToken(token);
|
|
3101
3371
|
if (featureGate) {
|
|
3102
3372
|
await featureGate.primeEntitlements();
|
|
3103
3373
|
}
|
|
3104
|
-
debugRest('token.update.end');
|
|
3105
3374
|
},
|
|
3106
3375
|
};
|
|
3107
3376
|
}
|