@oml/server 0.14.2 → 0.14.4
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 +1 -1
- package/out/auth/feature-gate.d.ts +8 -4
- package/out/auth/feature-gate.js +132 -57
- package/out/auth/feature-gate.js.map +1 -1
- package/out/auth/feature-policy.d.ts +3 -2
- package/out/auth/feature-policy.js +14 -5
- package/out/auth/feature-policy.js.map +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/lsp/language-server.d.ts +1 -1
- package/out/lsp/language-server.js +26 -5
- package/out/lsp/language-server.js.map +1 -1
- package/out/lsp/protocol/reasoner-protocol.d.ts +14 -0
- package/out/lsp/protocol/reasoner-protocol.js +1 -0
- package/out/lsp/protocol/reasoner-protocol.js.map +1 -1
- package/out/rest/routes.d.ts +0 -5
- package/out/rest/routes.js +374 -10
- package/out/rest/routes.js.map +1 -1
- package/out/rest/server.js +459 -66
- package/out/rest/server.js.map +1 -1
- package/out/{cli.js → server.js} +57 -8
- package/out/server.js.map +1 -0
- package/package.json +12 -8
- package/out/cli.js.map +0 -1
- /package/out/{cli.d.ts → server.d.ts} +0 -0
package/out/rest/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import { randomUUID, createHash } from 'node:crypto';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import { PassThrough } from 'node:stream';
|
|
9
|
-
import { URL, pathToFileURL } from 'node:url';
|
|
9
|
+
import { URL, fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
10
|
import { URI } from 'langium';
|
|
11
11
|
import { NodeFileSystem } from 'langium/node';
|
|
12
12
|
import { createConnection } from 'vscode-languageserver/node.js';
|
|
@@ -26,6 +26,35 @@ const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
|
26
26
|
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
27
27
|
const DEFAULT_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
28
28
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
29
|
+
const require = createModuleRequire();
|
|
30
|
+
const SWAGGER_ASSET_FILES = new Set([
|
|
31
|
+
'swagger-ui.css',
|
|
32
|
+
'swagger-ui-bundle.js',
|
|
33
|
+
'swagger-ui-standalone-preset.js',
|
|
34
|
+
'favicon-32x32.png',
|
|
35
|
+
'favicon-16x16.png',
|
|
36
|
+
]);
|
|
37
|
+
const SWAGGER_CONTENT_TYPES = {
|
|
38
|
+
'.css': 'text/css; charset=utf-8',
|
|
39
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
40
|
+
'.png': 'image/png',
|
|
41
|
+
};
|
|
42
|
+
let swaggerDistDirCache;
|
|
43
|
+
function createModuleRequire() {
|
|
44
|
+
if (typeof __filename === 'string') {
|
|
45
|
+
return createRequire(__filename);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const metaUrl = (0, eval)('import.meta.url');
|
|
49
|
+
if (typeof metaUrl === 'string' && metaUrl.length > 0) {
|
|
50
|
+
return createRequire(fileURLToPath(metaUrl));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Fall through to cwd-based anchor.
|
|
55
|
+
}
|
|
56
|
+
return createRequire(path.join(process.cwd(), '__oml_require_anchor__.cjs'));
|
|
57
|
+
}
|
|
29
58
|
const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
30
59
|
'table',
|
|
31
60
|
'tree',
|
|
@@ -37,6 +66,13 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
|
37
66
|
'matrix',
|
|
38
67
|
'table-editor'
|
|
39
68
|
]);
|
|
69
|
+
class RestHttpError extends Error {
|
|
70
|
+
constructor(statusCode, message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'RestHttpError';
|
|
73
|
+
this.statusCode = statusCode;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
40
76
|
const BUILT_IN_ONTOLOGIES = new Set([
|
|
41
77
|
'http://www.w3.org/2001/XMLSchema',
|
|
42
78
|
'http://www.w3.org/1999/02/22-rdf-syntax-ns',
|
|
@@ -52,6 +88,79 @@ function jsonResponse(res, status, payload) {
|
|
|
52
88
|
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
53
89
|
res.end(body);
|
|
54
90
|
}
|
|
91
|
+
function textResponse(res, status, contentType, body) {
|
|
92
|
+
res.statusCode = status;
|
|
93
|
+
res.setHeader('content-type', `${contentType}; charset=utf-8`);
|
|
94
|
+
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
95
|
+
res.end(body);
|
|
96
|
+
}
|
|
97
|
+
function resolveSwaggerDistDir() {
|
|
98
|
+
if (!swaggerDistDirCache) {
|
|
99
|
+
const swaggerPackageJson = require.resolve('swagger-ui-dist/package.json');
|
|
100
|
+
swaggerDistDirCache = path.dirname(swaggerPackageJson);
|
|
101
|
+
}
|
|
102
|
+
return swaggerDistDirCache;
|
|
103
|
+
}
|
|
104
|
+
async function serveSwaggerAsset(res, fileName) {
|
|
105
|
+
if (!SWAGGER_ASSET_FILES.has(fileName)) {
|
|
106
|
+
jsonResponse(res, 404, { error: 'Swagger asset not found.' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const filePath = path.join(resolveSwaggerDistDir(), fileName);
|
|
110
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
111
|
+
const contentType = SWAGGER_CONTENT_TYPES[extension] ?? 'application/octet-stream';
|
|
112
|
+
const content = await fs.readFile(filePath);
|
|
113
|
+
res.statusCode = 200;
|
|
114
|
+
res.setHeader('content-type', contentType);
|
|
115
|
+
res.setHeader('cache-control', 'public, max-age=3600');
|
|
116
|
+
res.setHeader('content-length', content.length);
|
|
117
|
+
res.end(content);
|
|
118
|
+
}
|
|
119
|
+
function createSwaggerUiPage() {
|
|
120
|
+
return `<!doctype html>
|
|
121
|
+
<html lang="en">
|
|
122
|
+
<head>
|
|
123
|
+
<meta charset="utf-8">
|
|
124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
125
|
+
<title>OML REST API Docs</title>
|
|
126
|
+
<link rel="stylesheet" href="/docs/swagger-ui.css" />
|
|
127
|
+
<link rel="icon" type="image/png" href="/docs/favicon-32x32.png" sizes="32x32" />
|
|
128
|
+
<link rel="icon" type="image/png" href="/docs/favicon-16x16.png" sizes="16x16" />
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
<div id="swagger-ui"></div>
|
|
132
|
+
<script src="/docs/swagger-ui-bundle.js"></script>
|
|
133
|
+
<script src="/docs/swagger-ui-standalone-preset.js"></script>
|
|
134
|
+
<script>
|
|
135
|
+
window.ui = SwaggerUIBundle({
|
|
136
|
+
url: '/openapi.json',
|
|
137
|
+
dom_id: '#swagger-ui',
|
|
138
|
+
deepLinking: true,
|
|
139
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
140
|
+
layout: 'BaseLayout'
|
|
141
|
+
});
|
|
142
|
+
</script>
|
|
143
|
+
</body>
|
|
144
|
+
</html>`;
|
|
145
|
+
}
|
|
146
|
+
function resolveListeningPort(server) {
|
|
147
|
+
const address = server.address();
|
|
148
|
+
if (!address || typeof address === 'string') {
|
|
149
|
+
throw new Error('Unable to resolve listening port.');
|
|
150
|
+
}
|
|
151
|
+
return address.port;
|
|
152
|
+
}
|
|
153
|
+
function accessErrorPayload(error, requestId) {
|
|
154
|
+
return {
|
|
155
|
+
error: {
|
|
156
|
+
code: error.code.toLowerCase(),
|
|
157
|
+
message: error.message,
|
|
158
|
+
status: error.statusCode,
|
|
159
|
+
retryable: error.retryable,
|
|
160
|
+
},
|
|
161
|
+
requestId,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
55
164
|
function htmlResponse(res, status, body) {
|
|
56
165
|
res.statusCode = status;
|
|
57
166
|
res.setHeader('content-type', HTML_CONTENT_TYPE);
|
|
@@ -154,6 +263,14 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
154
263
|
padding: 20px 24px;
|
|
155
264
|
}
|
|
156
265
|
|
|
266
|
+
.hero-header {
|
|
267
|
+
display: flex;
|
|
268
|
+
justify-content: space-between;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: 14px;
|
|
271
|
+
flex-wrap: wrap;
|
|
272
|
+
}
|
|
273
|
+
|
|
157
274
|
.page-title {
|
|
158
275
|
margin: 0;
|
|
159
276
|
font-size: clamp(1.9rem, 3.4vw, 2.7rem);
|
|
@@ -168,6 +285,34 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
168
285
|
font-size: 0.98rem;
|
|
169
286
|
}
|
|
170
287
|
|
|
288
|
+
.swagger-link {
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
text-decoration: none;
|
|
293
|
+
border: 1px solid var(--line);
|
|
294
|
+
border-radius: 999px;
|
|
295
|
+
padding: 8px 12px;
|
|
296
|
+
font-size: 0.86rem;
|
|
297
|
+
font-weight: 700;
|
|
298
|
+
color: #344054;
|
|
299
|
+
background: #ffffff;
|
|
300
|
+
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.swagger-link:hover {
|
|
304
|
+
border-color: #b2ccff;
|
|
305
|
+
box-shadow: 0 8px 20px rgba(21, 94, 239, 0.16);
|
|
306
|
+
transform: translateY(-1px);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.swagger-link svg {
|
|
310
|
+
width: 16px;
|
|
311
|
+
height: 16px;
|
|
312
|
+
fill: #155eef;
|
|
313
|
+
flex: 0 0 auto;
|
|
314
|
+
}
|
|
315
|
+
|
|
171
316
|
.row {
|
|
172
317
|
background: var(--card);
|
|
173
318
|
border: 1px solid var(--line-soft);
|
|
@@ -556,7 +701,15 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
556
701
|
<body>
|
|
557
702
|
<div class="shell">
|
|
558
703
|
<section class="hero">
|
|
559
|
-
<
|
|
704
|
+
<div class="hero-header">
|
|
705
|
+
<h1 class="page-title">OML Server</h1>
|
|
706
|
+
<a class="swagger-link" href="/docs" target="_blank" rel="noopener noreferrer" title="Open Swagger Docs">
|
|
707
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
708
|
+
<path d="M13.8 2.6a2.1 2.1 0 0 0-3.6 1.5c0 .2 0 .4.1.6a6 6 0 0 0-2.2 1.3l-.4-.2a2.1 2.1 0 1 0-1.8 3.8l.4.2a6.3 6.3 0 0 0 0 2.6l-.4.2a2.1 2.1 0 1 0 1.8 3.8l.4-.2a6 6 0 0 0 2.2 1.3 2.1 2.1 0 1 0 3.5 0 6 6 0 0 0 2.2-1.3l.4.2a2.1 2.1 0 1 0 1.8-3.8l-.4-.2a6.3 6.3 0 0 0 0-2.6l.4-.2a2.1 2.1 0 1 0-1.8-3.8l-.4.2a6 6 0 0 0-2.2-1.3c.1-.2.1-.4.1-.6 0-.6-.2-1.1-.6-1.5Zm-2.6 6.3a3.3 3.3 0 1 1 0 6.6 3.3 3.3 0 0 1 0-6.6Zm4.8 9.6a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm-9.6 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm11-8.7a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-12.4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm7.2-7.1a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"/>
|
|
709
|
+
</svg>
|
|
710
|
+
Swagger
|
|
711
|
+
</a>
|
|
712
|
+
</div>
|
|
560
713
|
<p class="page-subtitle">Run SPARQL queries against workspace models with fast browsing, paged table results, and raw JSON diagnostics.</p>
|
|
561
714
|
</section>
|
|
562
715
|
|
|
@@ -660,7 +813,7 @@ LIMIT 25</textarea>
|
|
|
660
813
|
const browseFilter = document.getElementById('browseFilter');
|
|
661
814
|
|
|
662
815
|
let browseEntries = [];
|
|
663
|
-
let
|
|
816
|
+
let selectedModelId = '';
|
|
664
817
|
let currentTableColumns = [];
|
|
665
818
|
let currentTableRows = [];
|
|
666
819
|
let currentPageIndex = 0;
|
|
@@ -693,8 +846,8 @@ LIMIT 25</textarea>
|
|
|
693
846
|
if (!term || typeof term !== 'object') {
|
|
694
847
|
return '';
|
|
695
848
|
}
|
|
696
|
-
if (term.
|
|
697
|
-
const suffix = term
|
|
849
|
+
if (term.type === 'literal') {
|
|
850
|
+
const suffix = term['xml:lang'] ? '@' + term['xml:lang'] : (term.datatype ? '^^' + term.datatype : '');
|
|
698
851
|
return '"' + term.value + '"' + suffix;
|
|
699
852
|
}
|
|
700
853
|
return String(term.value ?? '');
|
|
@@ -738,14 +891,15 @@ LIMIT 25</textarea>
|
|
|
738
891
|
setTableStatus('');
|
|
739
892
|
}
|
|
740
893
|
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
|
|
894
|
+
function renderSelectBindings(result) {
|
|
895
|
+
const vars = Array.isArray(result && result.head && result.head.vars) ? result.head.vars.filter((value) => typeof value === 'string') : [];
|
|
896
|
+
const bindings = Array.isArray(result && result.results && result.results.bindings) ? result.results.bindings : [];
|
|
897
|
+
if (bindings.length === 0) {
|
|
744
898
|
resetTable('The SELECT query returned no rows.');
|
|
745
899
|
return;
|
|
746
900
|
}
|
|
747
|
-
currentTableColumns = [...new Set(
|
|
748
|
-
currentTableRows =
|
|
901
|
+
currentTableColumns = vars.length > 0 ? vars : [...new Set(bindings.flatMap((row) => Object.keys(row)))];
|
|
902
|
+
currentTableRows = bindings;
|
|
749
903
|
currentPageIndex = 0;
|
|
750
904
|
renderTablePage();
|
|
751
905
|
}
|
|
@@ -756,9 +910,11 @@ LIMIT 25</textarea>
|
|
|
756
910
|
return;
|
|
757
911
|
}
|
|
758
912
|
browseList.innerHTML = entries.map((entry) => {
|
|
759
|
-
const
|
|
913
|
+
const normalizedPath = String(entry.path || '').trim();
|
|
914
|
+
const modelId = normalizedPath.replace(/\.oml$/i, '');
|
|
915
|
+
const isSelected = selectedModelId && selectedModelId === modelId;
|
|
760
916
|
const selectedClass = isSelected ? ' selected' : '';
|
|
761
|
-
return '<button type="button" class="browse-entry' + selectedClass + '" data-
|
|
917
|
+
return '<button type="button" class="browse-entry' + selectedClass + '" data-model-id="' + escapeClientHtml(modelId) + '" data-path="' + escapeClientHtml(entry.path) + '">'
|
|
762
918
|
+ '<div class="browse-entry-path" title="' + escapeClientHtml(entry.path) + '">' + escapeClientHtml(entry.path) + '</div>'
|
|
763
919
|
+ '</button>';
|
|
764
920
|
}).join('');
|
|
@@ -793,29 +949,23 @@ LIMIT 25</textarea>
|
|
|
793
949
|
}
|
|
794
950
|
}
|
|
795
951
|
|
|
796
|
-
function
|
|
797
|
-
if (!
|
|
952
|
+
function validateModelId(modelId) {
|
|
953
|
+
if (!modelId) {
|
|
798
954
|
return 'OML file is required.';
|
|
799
955
|
}
|
|
800
|
-
if (
|
|
801
|
-
return '
|
|
802
|
-
}
|
|
803
|
-
if (modelUri.toLowerCase().endsWith('.md')) {
|
|
804
|
-
return 'Markdown files are not queryable directly. Choose an .oml file or resolve the markdown document\\'s ontology context first.';
|
|
805
|
-
}
|
|
806
|
-
if (!modelUri.toLowerCase().endsWith('.oml')) {
|
|
807
|
-
return 'Choose an .oml model file.';
|
|
956
|
+
if (modelId.toLowerCase().endsWith('.md')) {
|
|
957
|
+
return 'Markdown files are not queryable directly. Choose an .oml file.';
|
|
808
958
|
}
|
|
809
959
|
return undefined;
|
|
810
960
|
}
|
|
811
961
|
|
|
812
962
|
async function executeQuery() {
|
|
813
|
-
const
|
|
963
|
+
const modelId = selectedModelId.trim();
|
|
814
964
|
const sparql = sparqlInput.value.trim();
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
817
|
-
setStatus(
|
|
818
|
-
rawOutput.textContent = JSON.stringify({ error:
|
|
965
|
+
const modelIdError = validateModelId(modelId);
|
|
966
|
+
if (modelIdError) {
|
|
967
|
+
setStatus(modelIdError, 'error');
|
|
968
|
+
rawOutput.textContent = JSON.stringify({ error: modelIdError }, null, 2);
|
|
819
969
|
resetTable('Choose a valid OML model file to run a query.');
|
|
820
970
|
browseModelsButton.focus();
|
|
821
971
|
return;
|
|
@@ -832,36 +982,50 @@ LIMIT 25</textarea>
|
|
|
832
982
|
setStatus('Running query...', '');
|
|
833
983
|
|
|
834
984
|
try {
|
|
835
|
-
const response = await fetch('/v0/
|
|
985
|
+
const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
|
|
836
986
|
method: 'POST',
|
|
837
|
-
headers: {
|
|
838
|
-
|
|
987
|
+
headers: {
|
|
988
|
+
'content-type': 'application/sparql-query',
|
|
989
|
+
'accept': 'application/sparql-results+json, text/turtle, application/n-triples;q=0.9'
|
|
990
|
+
},
|
|
991
|
+
body: sparql
|
|
839
992
|
});
|
|
840
|
-
const
|
|
841
|
-
|
|
993
|
+
const contentType = (response.headers.get('content-type') || '').toLowerCase();
|
|
994
|
+
const rawBody = await response.text();
|
|
995
|
+
let payload = undefined;
|
|
996
|
+
if (contentType.includes('application/json') || contentType.includes('application/sparql-results+json')) {
|
|
997
|
+
try {
|
|
998
|
+
payload = JSON.parse(rawBody);
|
|
999
|
+
} catch {
|
|
1000
|
+
payload = undefined;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
rawOutput.textContent = payload !== undefined ? JSON.stringify(payload, null, 2) : rawBody;
|
|
842
1004
|
|
|
843
1005
|
if (!response.ok) {
|
|
844
1006
|
resetTable('Raw response is shown below.');
|
|
845
|
-
|
|
1007
|
+
const payloadRecord = payload && typeof payload === 'object' ? payload : undefined;
|
|
1008
|
+
const payloadError = payloadRecord && typeof payloadRecord.error === 'object' && payloadRecord.error !== null
|
|
1009
|
+
? payloadRecord.error
|
|
1010
|
+
: undefined;
|
|
1011
|
+
const errorMessage = payloadError && typeof payloadError.message === 'string'
|
|
1012
|
+
? payloadError.message
|
|
1013
|
+
: (payloadRecord && typeof payloadRecord.error === 'string'
|
|
1014
|
+
? payloadRecord.error
|
|
1015
|
+
: (rawBody || 'Request failed.'));
|
|
1016
|
+
setStatus(errorMessage, 'error');
|
|
846
1017
|
return;
|
|
847
1018
|
}
|
|
848
1019
|
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
if (
|
|
852
|
-
|
|
1020
|
+
const isSparqlJson = contentType.includes('application/sparql-results+json');
|
|
1021
|
+
const isSelectResult = isSparqlJson && payload && typeof payload === 'object' && payload.results && payload.head;
|
|
1022
|
+
if (isSelectResult) {
|
|
1023
|
+
renderSelectBindings(payload);
|
|
853
1024
|
} else {
|
|
854
1025
|
resetTable('Raw response is shown below. Table rendering is available only for SELECT queries.');
|
|
855
1026
|
}
|
|
856
1027
|
|
|
857
|
-
|
|
858
|
-
setStatus(result.error || 'Query completed with an error.', 'error');
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const warnings = Array.isArray(result && result.warnings) ? result.warnings.length : 0;
|
|
863
|
-
const warningSuffix = warnings > 0 ? ' ' + warnings + ' warning(s).' : '';
|
|
864
|
-
setStatus('Query completed as ' + kind.toUpperCase() + '.' + warningSuffix, 'success');
|
|
1028
|
+
setStatus('Query completed.', 'success');
|
|
865
1029
|
} catch (error) {
|
|
866
1030
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
867
1031
|
resetTable('Raw response is shown below.');
|
|
@@ -885,16 +1049,16 @@ LIMIT 25</textarea>
|
|
|
885
1049
|
});
|
|
886
1050
|
|
|
887
1051
|
browseList.addEventListener('click', (event) => {
|
|
888
|
-
const target = event.target && event.target.closest ? event.target.closest('button[data-
|
|
1052
|
+
const target = event.target && event.target.closest ? event.target.closest('button[data-model-id]') : null;
|
|
889
1053
|
if (!target) {
|
|
890
1054
|
return;
|
|
891
1055
|
}
|
|
892
|
-
const
|
|
1056
|
+
const modelId = target.getAttribute('data-model-id');
|
|
893
1057
|
const relativePath = target.getAttribute('data-path');
|
|
894
|
-
if (!
|
|
1058
|
+
if (!modelId) {
|
|
895
1059
|
return;
|
|
896
1060
|
}
|
|
897
|
-
|
|
1061
|
+
selectedModelId = modelId;
|
|
898
1062
|
modelUriInput.value = relativePath || '';
|
|
899
1063
|
setStatus('Selected workspace OML file.', 'success');
|
|
900
1064
|
applyBrowseFilter();
|
|
@@ -934,6 +1098,14 @@ LIMIT 25</textarea>
|
|
|
934
1098
|
</html>`;
|
|
935
1099
|
}
|
|
936
1100
|
async function readJsonBody(req) {
|
|
1101
|
+
const text = await readTextBody(req);
|
|
1102
|
+
if (!text) {
|
|
1103
|
+
return {};
|
|
1104
|
+
}
|
|
1105
|
+
const parsed = JSON.parse(text);
|
|
1106
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
1107
|
+
}
|
|
1108
|
+
async function readTextBody(req) {
|
|
937
1109
|
const chunks = [];
|
|
938
1110
|
let total = 0;
|
|
939
1111
|
for await (const chunk of req) {
|
|
@@ -945,10 +1117,141 @@ async function readJsonBody(req) {
|
|
|
945
1117
|
chunks.push(buffer);
|
|
946
1118
|
}
|
|
947
1119
|
if (chunks.length === 0) {
|
|
1120
|
+
return '';
|
|
1121
|
+
}
|
|
1122
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
1123
|
+
}
|
|
1124
|
+
function parseContentType(raw) {
|
|
1125
|
+
if (!raw) {
|
|
1126
|
+
return '';
|
|
1127
|
+
}
|
|
1128
|
+
return raw.split(';', 1)[0]?.trim().toLowerCase() ?? '';
|
|
1129
|
+
}
|
|
1130
|
+
function resolveSparqlModelPathFromRoute(pathname) {
|
|
1131
|
+
const prefix = '/v0/sparql/';
|
|
1132
|
+
if (!pathname.startsWith(prefix)) {
|
|
1133
|
+
return undefined;
|
|
1134
|
+
}
|
|
1135
|
+
const modelPath = decodeURIComponent(pathname.slice(prefix.length)).trim();
|
|
1136
|
+
return modelPath.length > 0 ? modelPath : undefined;
|
|
1137
|
+
}
|
|
1138
|
+
function resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath) {
|
|
1139
|
+
const normalized = routeModelPath.replace(/^\/+|\/+$/g, '');
|
|
1140
|
+
if (!normalized) {
|
|
1141
|
+
throw new RestHttpError(400, 'Missing model path in SPARQL endpoint route.');
|
|
1142
|
+
}
|
|
1143
|
+
if (path.isAbsolute(normalized)) {
|
|
1144
|
+
throw new RestHttpError(400, 'SPARQL model path must be workspace-relative.');
|
|
1145
|
+
}
|
|
1146
|
+
if (normalized.toLowerCase().endsWith('.oml')) {
|
|
1147
|
+
throw new RestHttpError(400, "SPARQL model path must not include '.oml'. Use workspace-relative path without extension.");
|
|
1148
|
+
}
|
|
1149
|
+
const candidate = path.resolve(workspaceRoot, `${normalized}.oml`);
|
|
1150
|
+
const normalizedWorkspace = path.resolve(workspaceRoot);
|
|
1151
|
+
if (candidate !== normalizedWorkspace && !candidate.startsWith(`${normalizedWorkspace}${path.sep}`)) {
|
|
1152
|
+
throw new RestHttpError(400, 'SPARQL model path escapes the workspace root.');
|
|
1153
|
+
}
|
|
1154
|
+
return pathToFileURL(candidate).toString();
|
|
1155
|
+
}
|
|
1156
|
+
async function assertModelUriExists(modelUri) {
|
|
1157
|
+
const filePath = fileURLToPath(modelUri);
|
|
1158
|
+
await fs.access(filePath, fsSync.constants.F_OK);
|
|
1159
|
+
}
|
|
1160
|
+
function parseSparqlQueryFromRequest(req, parsedUrl, bodyText) {
|
|
1161
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
1162
|
+
if (method === 'GET') {
|
|
1163
|
+
return parsedUrl.searchParams.get('query')?.trim() ?? '';
|
|
1164
|
+
}
|
|
1165
|
+
const contentType = parseContentType(req.headers['content-type']);
|
|
1166
|
+
if (contentType === 'application/sparql-query') {
|
|
1167
|
+
return bodyText.trim();
|
|
1168
|
+
}
|
|
1169
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
1170
|
+
return new URLSearchParams(bodyText).get('query')?.trim() ?? '';
|
|
1171
|
+
}
|
|
1172
|
+
throw new RestHttpError(400, `Unsupported Content-Type '${contentType || '(missing)'}' for SPARQL endpoint.`);
|
|
1173
|
+
}
|
|
1174
|
+
function sparqlJsonBinding(term) {
|
|
1175
|
+
if (!term || typeof term !== 'object') {
|
|
948
1176
|
return {};
|
|
949
1177
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1178
|
+
if (term.termType === 'NamedNode') {
|
|
1179
|
+
return { type: 'uri', value: String(term.value ?? '') };
|
|
1180
|
+
}
|
|
1181
|
+
if (term.termType === 'BlankNode') {
|
|
1182
|
+
return { type: 'bnode', value: String(term.value ?? '') };
|
|
1183
|
+
}
|
|
1184
|
+
if (term.termType === 'Literal') {
|
|
1185
|
+
const binding = {
|
|
1186
|
+
type: 'literal',
|
|
1187
|
+
value: String(term.value ?? ''),
|
|
1188
|
+
};
|
|
1189
|
+
const datatype = typeof term.datatype?.value === 'string' ? term.datatype.value : '';
|
|
1190
|
+
const language = typeof term.language === 'string' ? term.language : '';
|
|
1191
|
+
if (language) {
|
|
1192
|
+
binding['xml:lang'] = language;
|
|
1193
|
+
}
|
|
1194
|
+
else if (datatype) {
|
|
1195
|
+
binding.datatype = datatype;
|
|
1196
|
+
}
|
|
1197
|
+
return binding;
|
|
1198
|
+
}
|
|
1199
|
+
return { type: 'literal', value: String(term.value ?? '') };
|
|
1200
|
+
}
|
|
1201
|
+
function selectAcceptsSparqlJson(acceptHeader) {
|
|
1202
|
+
const normalized = (acceptHeader ?? '*/*').toLowerCase();
|
|
1203
|
+
return normalized.includes('*/*')
|
|
1204
|
+
|| normalized.includes('application/sparql-results+json')
|
|
1205
|
+
|| normalized.includes('application/json');
|
|
1206
|
+
}
|
|
1207
|
+
function selectGraphResponseFormat(acceptHeader) {
|
|
1208
|
+
const normalized = (acceptHeader ?? '*/*').toLowerCase();
|
|
1209
|
+
if (normalized.includes('application/n-triples')) {
|
|
1210
|
+
return { contentType: 'application/n-triples', format: 'nt' };
|
|
1211
|
+
}
|
|
1212
|
+
if (normalized.includes('*/*') || normalized.includes('text/turtle')) {
|
|
1213
|
+
return { contentType: 'text/turtle', format: 'ttl' };
|
|
1214
|
+
}
|
|
1215
|
+
return undefined;
|
|
1216
|
+
}
|
|
1217
|
+
async function executeSparqlProtocolQuery(client, modelUri, sparql) {
|
|
1218
|
+
const kind = detectSparqlKind(sparql);
|
|
1219
|
+
if (kind === 'select') {
|
|
1220
|
+
const result = await client.queryWorkspaceRaw(modelUri, sparql);
|
|
1221
|
+
return { kind, result };
|
|
1222
|
+
}
|
|
1223
|
+
if (kind === 'ask') {
|
|
1224
|
+
const result = await client.askWorkspaceRaw(modelUri, sparql);
|
|
1225
|
+
return { kind, result };
|
|
1226
|
+
}
|
|
1227
|
+
if (kind === 'construct' || kind === 'describe') {
|
|
1228
|
+
const result = await client.constructWorkspaceRaw(modelUri, sparql);
|
|
1229
|
+
return { kind, result };
|
|
1230
|
+
}
|
|
1231
|
+
throw new RestHttpError(400, 'Unsupported or unknown SPARQL query kind.');
|
|
1232
|
+
}
|
|
1233
|
+
function writeSparqlSelectJsonResponse(res, rows) {
|
|
1234
|
+
const vars = new Set();
|
|
1235
|
+
const bindings = rows.map((row) => {
|
|
1236
|
+
const entry = {};
|
|
1237
|
+
for (const [name, term] of row.entries()) {
|
|
1238
|
+
vars.add(name);
|
|
1239
|
+
entry[name] = sparqlJsonBinding(term);
|
|
1240
|
+
}
|
|
1241
|
+
return entry;
|
|
1242
|
+
});
|
|
1243
|
+
const payload = {
|
|
1244
|
+
head: { vars: Array.from(vars) },
|
|
1245
|
+
results: { bindings },
|
|
1246
|
+
};
|
|
1247
|
+
textResponse(res, 200, 'application/sparql-results+json', JSON.stringify(payload));
|
|
1248
|
+
}
|
|
1249
|
+
function writeSparqlAskJsonResponse(res, value) {
|
|
1250
|
+
const payload = {
|
|
1251
|
+
head: {},
|
|
1252
|
+
boolean: value,
|
|
1253
|
+
};
|
|
1254
|
+
textResponse(res, 200, 'application/sparql-results+json', JSON.stringify(payload));
|
|
952
1255
|
}
|
|
953
1256
|
function withTimeout(promise, timeoutMs, method) {
|
|
954
1257
|
return new Promise((resolve, reject) => {
|
|
@@ -1274,7 +1577,6 @@ async function writeBlockArtifacts(outputFile, results) {
|
|
|
1274
1577
|
return { count: results.length, manifest };
|
|
1275
1578
|
}
|
|
1276
1579
|
async function loadStaticRuntimeBundle() {
|
|
1277
|
-
const require = createRequire(import.meta.url);
|
|
1278
1580
|
const staticEntry = require.resolve('@oml/markdown/static');
|
|
1279
1581
|
const bundlePath = path.join(path.dirname(staticEntry), STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE);
|
|
1280
1582
|
try {
|
|
@@ -1285,7 +1587,6 @@ async function loadStaticRuntimeBundle() {
|
|
|
1285
1587
|
}
|
|
1286
1588
|
}
|
|
1287
1589
|
async function loadCodeBlockStylesheet() {
|
|
1288
|
-
const require = createRequire(import.meta.url);
|
|
1289
1590
|
const staticEntry = require.resolve('@oml/markdown/static');
|
|
1290
1591
|
const stylesheetPath = path.resolve(path.dirname(staticEntry), '..', '..', 'src', 'static', 'markdown-webview.css');
|
|
1291
1592
|
try {
|
|
@@ -1506,9 +1807,28 @@ class InMemoryJsonRpcLspClient {
|
|
|
1506
1807
|
};
|
|
1507
1808
|
return await lintWorkspace(context, params);
|
|
1508
1809
|
}
|
|
1509
|
-
async
|
|
1810
|
+
async queryWorkspaceRaw(modelUri, sparql) {
|
|
1811
|
+
await this.ensureInitialized();
|
|
1812
|
+
await this.ensureWorkspaceCurrent();
|
|
1813
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1814
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1815
|
+
return await reasoningService.getSparqlService().query(modelUri, sparql);
|
|
1816
|
+
}
|
|
1817
|
+
async askWorkspaceRaw(modelUri, sparql) {
|
|
1818
|
+
await this.ensureInitialized();
|
|
1819
|
+
await this.ensureWorkspaceCurrent();
|
|
1820
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1821
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1822
|
+
return await reasoningService.getSparqlService().ask(modelUri, sparql);
|
|
1823
|
+
}
|
|
1824
|
+
async constructWorkspaceRaw(modelUri, sparql) {
|
|
1510
1825
|
await this.ensureInitialized();
|
|
1511
1826
|
await this.ensureWorkspaceCurrent();
|
|
1827
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1828
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1829
|
+
return await reasoningService.getSparqlService().construct(modelUri, sparql);
|
|
1830
|
+
}
|
|
1831
|
+
async queryWorkspace(params) {
|
|
1512
1832
|
const modelUri = typeof params.modelUri === 'string' ? params.modelUri.trim() : '';
|
|
1513
1833
|
const sparql = typeof params.sparql === 'string' ? params.sparql : '';
|
|
1514
1834
|
if (!modelUri) {
|
|
@@ -1520,12 +1840,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
1520
1840
|
};
|
|
1521
1841
|
}
|
|
1522
1842
|
try {
|
|
1523
|
-
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1524
1843
|
const kind = detectSparqlKind(sparql);
|
|
1525
|
-
await reasoningService.ensureQueryContext(modelUri);
|
|
1526
|
-
const sparqlService = reasoningService.getSparqlService();
|
|
1527
1844
|
if (kind === 'select') {
|
|
1528
|
-
const result = await
|
|
1845
|
+
const result = await this.queryWorkspaceRaw(modelUri, sparql);
|
|
1529
1846
|
return {
|
|
1530
1847
|
success: result.success,
|
|
1531
1848
|
kind,
|
|
@@ -1535,7 +1852,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1535
1852
|
};
|
|
1536
1853
|
}
|
|
1537
1854
|
if (kind === 'ask') {
|
|
1538
|
-
const result = await
|
|
1855
|
+
const result = await this.askWorkspaceRaw(modelUri, sparql);
|
|
1539
1856
|
return {
|
|
1540
1857
|
success: result.success,
|
|
1541
1858
|
kind,
|
|
@@ -1545,7 +1862,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1545
1862
|
};
|
|
1546
1863
|
}
|
|
1547
1864
|
if (kind === 'construct' || kind === 'describe') {
|
|
1548
|
-
const result = await
|
|
1865
|
+
const result = await this.constructWorkspaceRaw(modelUri, sparql);
|
|
1549
1866
|
return {
|
|
1550
1867
|
success: result.success,
|
|
1551
1868
|
kind,
|
|
@@ -2172,12 +2489,12 @@ class InMemoryJsonRpcLspClient {
|
|
|
2172
2489
|
export async function startOmlRestServer(options) {
|
|
2173
2490
|
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2174
2491
|
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.watchWorkspace === true, options.runtime);
|
|
2175
|
-
|
|
2492
|
+
let openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2176
2493
|
let currentAccessToken = options.authToken;
|
|
2177
2494
|
const featureGate = options.featureGate;
|
|
2178
2495
|
if (featureGate && currentAccessToken) {
|
|
2179
2496
|
featureGate.setAccessToken(currentAccessToken);
|
|
2180
|
-
|
|
2497
|
+
void featureGate.primeEntitlements().catch(() => undefined);
|
|
2181
2498
|
}
|
|
2182
2499
|
const getAccessToken = () => currentAccessToken;
|
|
2183
2500
|
const server = http.createServer(async (req, res) => {
|
|
@@ -2206,14 +2523,82 @@ export async function startOmlRestServer(options) {
|
|
|
2206
2523
|
jsonResponse(res, 200, openApiSpec);
|
|
2207
2524
|
return;
|
|
2208
2525
|
}
|
|
2526
|
+
if (method === 'GET' && (pathname === '/docs' || pathname === '/docs/')) {
|
|
2527
|
+
htmlResponse(res, 200, createSwaggerUiPage());
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (method === 'GET' && pathname.startsWith('/docs/')) {
|
|
2531
|
+
const assetName = decodeURIComponent(pathname.slice('/docs/'.length)).trim();
|
|
2532
|
+
if (!assetName || assetName.includes('/') || assetName.includes('\\')) {
|
|
2533
|
+
jsonResponse(res, 404, { error: 'Swagger asset not found.', requestId });
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
await serveSwaggerAsset(res, assetName);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2209
2539
|
if (method === 'GET' && pathname === '/v0/models') {
|
|
2210
|
-
const
|
|
2540
|
+
const requiredFeature = requiredFeatureForRestOperation('models');
|
|
2541
|
+
const workspaceModelFiles = (featureGate && requiredFeature)
|
|
2542
|
+
? await featureGate.runWithFeature(requiredFeature, () => listWorkspaceModelFiles(workspaceRoot), { transport: 'rest', operationId: 'models' })
|
|
2543
|
+
: await listWorkspaceModelFiles(workspaceRoot);
|
|
2211
2544
|
jsonResponse(res, 200, {
|
|
2212
2545
|
files: workspaceModelFiles,
|
|
2213
2546
|
requestId,
|
|
2214
2547
|
});
|
|
2215
2548
|
return;
|
|
2216
2549
|
}
|
|
2550
|
+
if ((method === 'GET' || method === 'POST') && pathname.startsWith('/v0/sparql/')) {
|
|
2551
|
+
const runSparql = async () => {
|
|
2552
|
+
const routeModelPath = resolveSparqlModelPathFromRoute(pathname);
|
|
2553
|
+
if (!routeModelPath) {
|
|
2554
|
+
throw new RestHttpError(404, `No route for ${method} ${pathname}.`);
|
|
2555
|
+
}
|
|
2556
|
+
const modelUri = resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath);
|
|
2557
|
+
try {
|
|
2558
|
+
await assertModelUriExists(modelUri);
|
|
2559
|
+
}
|
|
2560
|
+
catch {
|
|
2561
|
+
throw new RestHttpError(404, `No OML model found for '${routeModelPath}'.`);
|
|
2562
|
+
}
|
|
2563
|
+
const bodyText = method === 'POST' ? await readTextBody(req) : '';
|
|
2564
|
+
const sparql = parseSparqlQueryFromRequest(req, parsed, bodyText);
|
|
2565
|
+
if (!sparql) {
|
|
2566
|
+
throw new RestHttpError(400, "Missing SPARQL query. Provide 'query' in URL params or request body.");
|
|
2567
|
+
}
|
|
2568
|
+
const queryResult = await executeSparqlProtocolQuery(client, modelUri, sparql);
|
|
2569
|
+
if (!queryResult.result.success) {
|
|
2570
|
+
throw new RestHttpError(400, queryResult.result.error || 'SPARQL query failed.');
|
|
2571
|
+
}
|
|
2572
|
+
if (queryResult.kind === 'select') {
|
|
2573
|
+
if (!selectAcceptsSparqlJson(req.headers.accept)) {
|
|
2574
|
+
throw new RestHttpError(406, 'Not Acceptable for SELECT results.');
|
|
2575
|
+
}
|
|
2576
|
+
writeSparqlSelectJsonResponse(res, queryResult.result.rows);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (queryResult.kind === 'ask') {
|
|
2580
|
+
if (!selectAcceptsSparqlJson(req.headers.accept)) {
|
|
2581
|
+
throw new RestHttpError(406, 'Not Acceptable for ASK results.');
|
|
2582
|
+
}
|
|
2583
|
+
writeSparqlAskJsonResponse(res, queryResult.result.result);
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
const graphFormat = selectGraphResponseFormat(req.headers.accept);
|
|
2587
|
+
if (!graphFormat) {
|
|
2588
|
+
throw new RestHttpError(406, 'Not Acceptable for graph results.');
|
|
2589
|
+
}
|
|
2590
|
+
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false);
|
|
2591
|
+
textResponse(res, 200, graphFormat.contentType, serialized);
|
|
2592
|
+
};
|
|
2593
|
+
const requiredFeature = requiredFeatureForRestOperation('query');
|
|
2594
|
+
if (featureGate && requiredFeature) {
|
|
2595
|
+
await featureGate.runWithFeature(requiredFeature, runSparql, { transport: 'rest', operationId: 'sparql' });
|
|
2596
|
+
}
|
|
2597
|
+
else {
|
|
2598
|
+
await runSparql();
|
|
2599
|
+
}
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2217
2602
|
if (method === 'POST') {
|
|
2218
2603
|
const body = await readJsonBody(req);
|
|
2219
2604
|
const operationId = resolveRestOperationId(method, pathname);
|
|
@@ -2235,9 +2620,15 @@ export async function startOmlRestServer(options) {
|
|
|
2235
2620
|
}
|
|
2236
2621
|
catch (error) {
|
|
2237
2622
|
const message = error instanceof Error ? error.message : String(error);
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2623
|
+
if (error instanceof OmlAccessError) {
|
|
2624
|
+
jsonResponse(res, error.statusCode, accessErrorPayload(error, requestId));
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
if (error instanceof RestHttpError) {
|
|
2628
|
+
jsonResponse(res, error.statusCode, { error: message, requestId });
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const status = error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500;
|
|
2241
2632
|
jsonResponse(res, status, { error: message, requestId });
|
|
2242
2633
|
}
|
|
2243
2634
|
});
|
|
@@ -2250,6 +2641,8 @@ export async function startOmlRestServer(options) {
|
|
|
2250
2641
|
server.once('error', reject);
|
|
2251
2642
|
server.listen(options.port, options.host, () => resolve());
|
|
2252
2643
|
});
|
|
2644
|
+
const listeningPort = resolveListeningPort(server);
|
|
2645
|
+
openApiSpec = createOpenApiSpec(options.host, listeningPort);
|
|
2253
2646
|
return {
|
|
2254
2647
|
server,
|
|
2255
2648
|
updateToken: async (token) => {
|