@oml/server 0.14.2 → 0.14.3
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/cli.js +56 -7
- package/out/cli.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 +444 -64
- package/out/rest/server.js.map +1 -1
- package/package.json +11 -8
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,20 @@ 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 = createRequire(import.meta.url);
|
|
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;
|
|
29
43
|
const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
30
44
|
'table',
|
|
31
45
|
'tree',
|
|
@@ -37,6 +51,13 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
|
37
51
|
'matrix',
|
|
38
52
|
'table-editor'
|
|
39
53
|
]);
|
|
54
|
+
class RestHttpError extends Error {
|
|
55
|
+
constructor(statusCode, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'RestHttpError';
|
|
58
|
+
this.statusCode = statusCode;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
40
61
|
const BUILT_IN_ONTOLOGIES = new Set([
|
|
41
62
|
'http://www.w3.org/2001/XMLSchema',
|
|
42
63
|
'http://www.w3.org/1999/02/22-rdf-syntax-ns',
|
|
@@ -52,6 +73,79 @@ function jsonResponse(res, status, payload) {
|
|
|
52
73
|
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
53
74
|
res.end(body);
|
|
54
75
|
}
|
|
76
|
+
function textResponse(res, status, contentType, body) {
|
|
77
|
+
res.statusCode = status;
|
|
78
|
+
res.setHeader('content-type', `${contentType}; charset=utf-8`);
|
|
79
|
+
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
80
|
+
res.end(body);
|
|
81
|
+
}
|
|
82
|
+
function resolveSwaggerDistDir() {
|
|
83
|
+
if (!swaggerDistDirCache) {
|
|
84
|
+
const swaggerPackageJson = require.resolve('swagger-ui-dist/package.json');
|
|
85
|
+
swaggerDistDirCache = path.dirname(swaggerPackageJson);
|
|
86
|
+
}
|
|
87
|
+
return swaggerDistDirCache;
|
|
88
|
+
}
|
|
89
|
+
async function serveSwaggerAsset(res, fileName) {
|
|
90
|
+
if (!SWAGGER_ASSET_FILES.has(fileName)) {
|
|
91
|
+
jsonResponse(res, 404, { error: 'Swagger asset not found.' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const filePath = path.join(resolveSwaggerDistDir(), fileName);
|
|
95
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
96
|
+
const contentType = SWAGGER_CONTENT_TYPES[extension] ?? 'application/octet-stream';
|
|
97
|
+
const content = await fs.readFile(filePath);
|
|
98
|
+
res.statusCode = 200;
|
|
99
|
+
res.setHeader('content-type', contentType);
|
|
100
|
+
res.setHeader('cache-control', 'public, max-age=3600');
|
|
101
|
+
res.setHeader('content-length', content.length);
|
|
102
|
+
res.end(content);
|
|
103
|
+
}
|
|
104
|
+
function createSwaggerUiPage() {
|
|
105
|
+
return `<!doctype html>
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="utf-8">
|
|
109
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
110
|
+
<title>OML REST API Docs</title>
|
|
111
|
+
<link rel="stylesheet" href="/docs/swagger-ui.css" />
|
|
112
|
+
<link rel="icon" type="image/png" href="/docs/favicon-32x32.png" sizes="32x32" />
|
|
113
|
+
<link rel="icon" type="image/png" href="/docs/favicon-16x16.png" sizes="16x16" />
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div id="swagger-ui"></div>
|
|
117
|
+
<script src="/docs/swagger-ui-bundle.js"></script>
|
|
118
|
+
<script src="/docs/swagger-ui-standalone-preset.js"></script>
|
|
119
|
+
<script>
|
|
120
|
+
window.ui = SwaggerUIBundle({
|
|
121
|
+
url: '/openapi.json',
|
|
122
|
+
dom_id: '#swagger-ui',
|
|
123
|
+
deepLinking: true,
|
|
124
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
125
|
+
layout: 'BaseLayout'
|
|
126
|
+
});
|
|
127
|
+
</script>
|
|
128
|
+
</body>
|
|
129
|
+
</html>`;
|
|
130
|
+
}
|
|
131
|
+
function resolveListeningPort(server) {
|
|
132
|
+
const address = server.address();
|
|
133
|
+
if (!address || typeof address === 'string') {
|
|
134
|
+
throw new Error('Unable to resolve listening port.');
|
|
135
|
+
}
|
|
136
|
+
return address.port;
|
|
137
|
+
}
|
|
138
|
+
function accessErrorPayload(error, requestId) {
|
|
139
|
+
return {
|
|
140
|
+
error: {
|
|
141
|
+
code: error.code.toLowerCase(),
|
|
142
|
+
message: error.message,
|
|
143
|
+
status: error.statusCode,
|
|
144
|
+
retryable: error.retryable,
|
|
145
|
+
},
|
|
146
|
+
requestId,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
55
149
|
function htmlResponse(res, status, body) {
|
|
56
150
|
res.statusCode = status;
|
|
57
151
|
res.setHeader('content-type', HTML_CONTENT_TYPE);
|
|
@@ -154,6 +248,14 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
154
248
|
padding: 20px 24px;
|
|
155
249
|
}
|
|
156
250
|
|
|
251
|
+
.hero-header {
|
|
252
|
+
display: flex;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 14px;
|
|
256
|
+
flex-wrap: wrap;
|
|
257
|
+
}
|
|
258
|
+
|
|
157
259
|
.page-title {
|
|
158
260
|
margin: 0;
|
|
159
261
|
font-size: clamp(1.9rem, 3.4vw, 2.7rem);
|
|
@@ -168,6 +270,34 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
168
270
|
font-size: 0.98rem;
|
|
169
271
|
}
|
|
170
272
|
|
|
273
|
+
.swagger-link {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 8px;
|
|
277
|
+
text-decoration: none;
|
|
278
|
+
border: 1px solid var(--line);
|
|
279
|
+
border-radius: 999px;
|
|
280
|
+
padding: 8px 12px;
|
|
281
|
+
font-size: 0.86rem;
|
|
282
|
+
font-weight: 700;
|
|
283
|
+
color: #344054;
|
|
284
|
+
background: #ffffff;
|
|
285
|
+
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.swagger-link:hover {
|
|
289
|
+
border-color: #b2ccff;
|
|
290
|
+
box-shadow: 0 8px 20px rgba(21, 94, 239, 0.16);
|
|
291
|
+
transform: translateY(-1px);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.swagger-link svg {
|
|
295
|
+
width: 16px;
|
|
296
|
+
height: 16px;
|
|
297
|
+
fill: #155eef;
|
|
298
|
+
flex: 0 0 auto;
|
|
299
|
+
}
|
|
300
|
+
|
|
171
301
|
.row {
|
|
172
302
|
background: var(--card);
|
|
173
303
|
border: 1px solid var(--line-soft);
|
|
@@ -556,7 +686,15 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
556
686
|
<body>
|
|
557
687
|
<div class="shell">
|
|
558
688
|
<section class="hero">
|
|
559
|
-
<
|
|
689
|
+
<div class="hero-header">
|
|
690
|
+
<h1 class="page-title">OML Server</h1>
|
|
691
|
+
<a class="swagger-link" href="/docs" target="_blank" rel="noopener noreferrer" title="Open Swagger Docs">
|
|
692
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
693
|
+
<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"/>
|
|
694
|
+
</svg>
|
|
695
|
+
Swagger
|
|
696
|
+
</a>
|
|
697
|
+
</div>
|
|
560
698
|
<p class="page-subtitle">Run SPARQL queries against workspace models with fast browsing, paged table results, and raw JSON diagnostics.</p>
|
|
561
699
|
</section>
|
|
562
700
|
|
|
@@ -660,7 +798,7 @@ LIMIT 25</textarea>
|
|
|
660
798
|
const browseFilter = document.getElementById('browseFilter');
|
|
661
799
|
|
|
662
800
|
let browseEntries = [];
|
|
663
|
-
let
|
|
801
|
+
let selectedModelId = '';
|
|
664
802
|
let currentTableColumns = [];
|
|
665
803
|
let currentTableRows = [];
|
|
666
804
|
let currentPageIndex = 0;
|
|
@@ -693,8 +831,8 @@ LIMIT 25</textarea>
|
|
|
693
831
|
if (!term || typeof term !== 'object') {
|
|
694
832
|
return '';
|
|
695
833
|
}
|
|
696
|
-
if (term.
|
|
697
|
-
const suffix = term
|
|
834
|
+
if (term.type === 'literal') {
|
|
835
|
+
const suffix = term['xml:lang'] ? '@' + term['xml:lang'] : (term.datatype ? '^^' + term.datatype : '');
|
|
698
836
|
return '"' + term.value + '"' + suffix;
|
|
699
837
|
}
|
|
700
838
|
return String(term.value ?? '');
|
|
@@ -738,14 +876,15 @@ LIMIT 25</textarea>
|
|
|
738
876
|
setTableStatus('');
|
|
739
877
|
}
|
|
740
878
|
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
|
|
879
|
+
function renderSelectBindings(result) {
|
|
880
|
+
const vars = Array.isArray(result && result.head && result.head.vars) ? result.head.vars.filter((value) => typeof value === 'string') : [];
|
|
881
|
+
const bindings = Array.isArray(result && result.results && result.results.bindings) ? result.results.bindings : [];
|
|
882
|
+
if (bindings.length === 0) {
|
|
744
883
|
resetTable('The SELECT query returned no rows.');
|
|
745
884
|
return;
|
|
746
885
|
}
|
|
747
|
-
currentTableColumns = [...new Set(
|
|
748
|
-
currentTableRows =
|
|
886
|
+
currentTableColumns = vars.length > 0 ? vars : [...new Set(bindings.flatMap((row) => Object.keys(row)))];
|
|
887
|
+
currentTableRows = bindings;
|
|
749
888
|
currentPageIndex = 0;
|
|
750
889
|
renderTablePage();
|
|
751
890
|
}
|
|
@@ -756,9 +895,11 @@ LIMIT 25</textarea>
|
|
|
756
895
|
return;
|
|
757
896
|
}
|
|
758
897
|
browseList.innerHTML = entries.map((entry) => {
|
|
759
|
-
const
|
|
898
|
+
const normalizedPath = String(entry.path || '').trim();
|
|
899
|
+
const modelId = normalizedPath.replace(/\.oml$/i, '');
|
|
900
|
+
const isSelected = selectedModelId && selectedModelId === modelId;
|
|
760
901
|
const selectedClass = isSelected ? ' selected' : '';
|
|
761
|
-
return '<button type="button" class="browse-entry' + selectedClass + '" data-
|
|
902
|
+
return '<button type="button" class="browse-entry' + selectedClass + '" data-model-id="' + escapeClientHtml(modelId) + '" data-path="' + escapeClientHtml(entry.path) + '">'
|
|
762
903
|
+ '<div class="browse-entry-path" title="' + escapeClientHtml(entry.path) + '">' + escapeClientHtml(entry.path) + '</div>'
|
|
763
904
|
+ '</button>';
|
|
764
905
|
}).join('');
|
|
@@ -793,29 +934,23 @@ LIMIT 25</textarea>
|
|
|
793
934
|
}
|
|
794
935
|
}
|
|
795
936
|
|
|
796
|
-
function
|
|
797
|
-
if (!
|
|
937
|
+
function validateModelId(modelId) {
|
|
938
|
+
if (!modelId) {
|
|
798
939
|
return 'OML file is required.';
|
|
799
940
|
}
|
|
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.';
|
|
941
|
+
if (modelId.toLowerCase().endsWith('.md')) {
|
|
942
|
+
return 'Markdown files are not queryable directly. Choose an .oml file.';
|
|
808
943
|
}
|
|
809
944
|
return undefined;
|
|
810
945
|
}
|
|
811
946
|
|
|
812
947
|
async function executeQuery() {
|
|
813
|
-
const
|
|
948
|
+
const modelId = selectedModelId.trim();
|
|
814
949
|
const sparql = sparqlInput.value.trim();
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
817
|
-
setStatus(
|
|
818
|
-
rawOutput.textContent = JSON.stringify({ error:
|
|
950
|
+
const modelIdError = validateModelId(modelId);
|
|
951
|
+
if (modelIdError) {
|
|
952
|
+
setStatus(modelIdError, 'error');
|
|
953
|
+
rawOutput.textContent = JSON.stringify({ error: modelIdError }, null, 2);
|
|
819
954
|
resetTable('Choose a valid OML model file to run a query.');
|
|
820
955
|
browseModelsButton.focus();
|
|
821
956
|
return;
|
|
@@ -832,36 +967,50 @@ LIMIT 25</textarea>
|
|
|
832
967
|
setStatus('Running query...', '');
|
|
833
968
|
|
|
834
969
|
try {
|
|
835
|
-
const response = await fetch('/v0/
|
|
970
|
+
const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
|
|
836
971
|
method: 'POST',
|
|
837
|
-
headers: {
|
|
838
|
-
|
|
972
|
+
headers: {
|
|
973
|
+
'content-type': 'application/sparql-query',
|
|
974
|
+
'accept': 'application/sparql-results+json, text/turtle, application/n-triples;q=0.9'
|
|
975
|
+
},
|
|
976
|
+
body: sparql
|
|
839
977
|
});
|
|
840
|
-
const
|
|
841
|
-
|
|
978
|
+
const contentType = (response.headers.get('content-type') || '').toLowerCase();
|
|
979
|
+
const rawBody = await response.text();
|
|
980
|
+
let payload = undefined;
|
|
981
|
+
if (contentType.includes('application/json') || contentType.includes('application/sparql-results+json')) {
|
|
982
|
+
try {
|
|
983
|
+
payload = JSON.parse(rawBody);
|
|
984
|
+
} catch {
|
|
985
|
+
payload = undefined;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
rawOutput.textContent = payload !== undefined ? JSON.stringify(payload, null, 2) : rawBody;
|
|
842
989
|
|
|
843
990
|
if (!response.ok) {
|
|
844
991
|
resetTable('Raw response is shown below.');
|
|
845
|
-
|
|
992
|
+
const payloadRecord = payload && typeof payload === 'object' ? payload : undefined;
|
|
993
|
+
const payloadError = payloadRecord && typeof payloadRecord.error === 'object' && payloadRecord.error !== null
|
|
994
|
+
? payloadRecord.error
|
|
995
|
+
: undefined;
|
|
996
|
+
const errorMessage = payloadError && typeof payloadError.message === 'string'
|
|
997
|
+
? payloadError.message
|
|
998
|
+
: (payloadRecord && typeof payloadRecord.error === 'string'
|
|
999
|
+
? payloadRecord.error
|
|
1000
|
+
: (rawBody || 'Request failed.'));
|
|
1001
|
+
setStatus(errorMessage, 'error');
|
|
846
1002
|
return;
|
|
847
1003
|
}
|
|
848
1004
|
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
if (
|
|
852
|
-
|
|
1005
|
+
const isSparqlJson = contentType.includes('application/sparql-results+json');
|
|
1006
|
+
const isSelectResult = isSparqlJson && payload && typeof payload === 'object' && payload.results && payload.head;
|
|
1007
|
+
if (isSelectResult) {
|
|
1008
|
+
renderSelectBindings(payload);
|
|
853
1009
|
} else {
|
|
854
1010
|
resetTable('Raw response is shown below. Table rendering is available only for SELECT queries.');
|
|
855
1011
|
}
|
|
856
1012
|
|
|
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');
|
|
1013
|
+
setStatus('Query completed.', 'success');
|
|
865
1014
|
} catch (error) {
|
|
866
1015
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
867
1016
|
resetTable('Raw response is shown below.');
|
|
@@ -885,16 +1034,16 @@ LIMIT 25</textarea>
|
|
|
885
1034
|
});
|
|
886
1035
|
|
|
887
1036
|
browseList.addEventListener('click', (event) => {
|
|
888
|
-
const target = event.target && event.target.closest ? event.target.closest('button[data-
|
|
1037
|
+
const target = event.target && event.target.closest ? event.target.closest('button[data-model-id]') : null;
|
|
889
1038
|
if (!target) {
|
|
890
1039
|
return;
|
|
891
1040
|
}
|
|
892
|
-
const
|
|
1041
|
+
const modelId = target.getAttribute('data-model-id');
|
|
893
1042
|
const relativePath = target.getAttribute('data-path');
|
|
894
|
-
if (!
|
|
1043
|
+
if (!modelId) {
|
|
895
1044
|
return;
|
|
896
1045
|
}
|
|
897
|
-
|
|
1046
|
+
selectedModelId = modelId;
|
|
898
1047
|
modelUriInput.value = relativePath || '';
|
|
899
1048
|
setStatus('Selected workspace OML file.', 'success');
|
|
900
1049
|
applyBrowseFilter();
|
|
@@ -934,6 +1083,14 @@ LIMIT 25</textarea>
|
|
|
934
1083
|
</html>`;
|
|
935
1084
|
}
|
|
936
1085
|
async function readJsonBody(req) {
|
|
1086
|
+
const text = await readTextBody(req);
|
|
1087
|
+
if (!text) {
|
|
1088
|
+
return {};
|
|
1089
|
+
}
|
|
1090
|
+
const parsed = JSON.parse(text);
|
|
1091
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
1092
|
+
}
|
|
1093
|
+
async function readTextBody(req) {
|
|
937
1094
|
const chunks = [];
|
|
938
1095
|
let total = 0;
|
|
939
1096
|
for await (const chunk of req) {
|
|
@@ -945,10 +1102,141 @@ async function readJsonBody(req) {
|
|
|
945
1102
|
chunks.push(buffer);
|
|
946
1103
|
}
|
|
947
1104
|
if (chunks.length === 0) {
|
|
1105
|
+
return '';
|
|
1106
|
+
}
|
|
1107
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
1108
|
+
}
|
|
1109
|
+
function parseContentType(raw) {
|
|
1110
|
+
if (!raw) {
|
|
1111
|
+
return '';
|
|
1112
|
+
}
|
|
1113
|
+
return raw.split(';', 1)[0]?.trim().toLowerCase() ?? '';
|
|
1114
|
+
}
|
|
1115
|
+
function resolveSparqlModelPathFromRoute(pathname) {
|
|
1116
|
+
const prefix = '/v0/sparql/';
|
|
1117
|
+
if (!pathname.startsWith(prefix)) {
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
const modelPath = decodeURIComponent(pathname.slice(prefix.length)).trim();
|
|
1121
|
+
return modelPath.length > 0 ? modelPath : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
function resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath) {
|
|
1124
|
+
const normalized = routeModelPath.replace(/^\/+|\/+$/g, '');
|
|
1125
|
+
if (!normalized) {
|
|
1126
|
+
throw new RestHttpError(400, 'Missing model path in SPARQL endpoint route.');
|
|
1127
|
+
}
|
|
1128
|
+
if (path.isAbsolute(normalized)) {
|
|
1129
|
+
throw new RestHttpError(400, 'SPARQL model path must be workspace-relative.');
|
|
1130
|
+
}
|
|
1131
|
+
if (normalized.toLowerCase().endsWith('.oml')) {
|
|
1132
|
+
throw new RestHttpError(400, "SPARQL model path must not include '.oml'. Use workspace-relative path without extension.");
|
|
1133
|
+
}
|
|
1134
|
+
const candidate = path.resolve(workspaceRoot, `${normalized}.oml`);
|
|
1135
|
+
const normalizedWorkspace = path.resolve(workspaceRoot);
|
|
1136
|
+
if (candidate !== normalizedWorkspace && !candidate.startsWith(`${normalizedWorkspace}${path.sep}`)) {
|
|
1137
|
+
throw new RestHttpError(400, 'SPARQL model path escapes the workspace root.');
|
|
1138
|
+
}
|
|
1139
|
+
return pathToFileURL(candidate).toString();
|
|
1140
|
+
}
|
|
1141
|
+
async function assertModelUriExists(modelUri) {
|
|
1142
|
+
const filePath = fileURLToPath(modelUri);
|
|
1143
|
+
await fs.access(filePath, fsSync.constants.F_OK);
|
|
1144
|
+
}
|
|
1145
|
+
function parseSparqlQueryFromRequest(req, parsedUrl, bodyText) {
|
|
1146
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
1147
|
+
if (method === 'GET') {
|
|
1148
|
+
return parsedUrl.searchParams.get('query')?.trim() ?? '';
|
|
1149
|
+
}
|
|
1150
|
+
const contentType = parseContentType(req.headers['content-type']);
|
|
1151
|
+
if (contentType === 'application/sparql-query') {
|
|
1152
|
+
return bodyText.trim();
|
|
1153
|
+
}
|
|
1154
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
1155
|
+
return new URLSearchParams(bodyText).get('query')?.trim() ?? '';
|
|
1156
|
+
}
|
|
1157
|
+
throw new RestHttpError(400, `Unsupported Content-Type '${contentType || '(missing)'}' for SPARQL endpoint.`);
|
|
1158
|
+
}
|
|
1159
|
+
function sparqlJsonBinding(term) {
|
|
1160
|
+
if (!term || typeof term !== 'object') {
|
|
948
1161
|
return {};
|
|
949
1162
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1163
|
+
if (term.termType === 'NamedNode') {
|
|
1164
|
+
return { type: 'uri', value: String(term.value ?? '') };
|
|
1165
|
+
}
|
|
1166
|
+
if (term.termType === 'BlankNode') {
|
|
1167
|
+
return { type: 'bnode', value: String(term.value ?? '') };
|
|
1168
|
+
}
|
|
1169
|
+
if (term.termType === 'Literal') {
|
|
1170
|
+
const binding = {
|
|
1171
|
+
type: 'literal',
|
|
1172
|
+
value: String(term.value ?? ''),
|
|
1173
|
+
};
|
|
1174
|
+
const datatype = typeof term.datatype?.value === 'string' ? term.datatype.value : '';
|
|
1175
|
+
const language = typeof term.language === 'string' ? term.language : '';
|
|
1176
|
+
if (language) {
|
|
1177
|
+
binding['xml:lang'] = language;
|
|
1178
|
+
}
|
|
1179
|
+
else if (datatype) {
|
|
1180
|
+
binding.datatype = datatype;
|
|
1181
|
+
}
|
|
1182
|
+
return binding;
|
|
1183
|
+
}
|
|
1184
|
+
return { type: 'literal', value: String(term.value ?? '') };
|
|
1185
|
+
}
|
|
1186
|
+
function selectAcceptsSparqlJson(acceptHeader) {
|
|
1187
|
+
const normalized = (acceptHeader ?? '*/*').toLowerCase();
|
|
1188
|
+
return normalized.includes('*/*')
|
|
1189
|
+
|| normalized.includes('application/sparql-results+json')
|
|
1190
|
+
|| normalized.includes('application/json');
|
|
1191
|
+
}
|
|
1192
|
+
function selectGraphResponseFormat(acceptHeader) {
|
|
1193
|
+
const normalized = (acceptHeader ?? '*/*').toLowerCase();
|
|
1194
|
+
if (normalized.includes('application/n-triples')) {
|
|
1195
|
+
return { contentType: 'application/n-triples', format: 'nt' };
|
|
1196
|
+
}
|
|
1197
|
+
if (normalized.includes('*/*') || normalized.includes('text/turtle')) {
|
|
1198
|
+
return { contentType: 'text/turtle', format: 'ttl' };
|
|
1199
|
+
}
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
1202
|
+
async function executeSparqlProtocolQuery(client, modelUri, sparql) {
|
|
1203
|
+
const kind = detectSparqlKind(sparql);
|
|
1204
|
+
if (kind === 'select') {
|
|
1205
|
+
const result = await client.queryWorkspaceRaw(modelUri, sparql);
|
|
1206
|
+
return { kind, result };
|
|
1207
|
+
}
|
|
1208
|
+
if (kind === 'ask') {
|
|
1209
|
+
const result = await client.askWorkspaceRaw(modelUri, sparql);
|
|
1210
|
+
return { kind, result };
|
|
1211
|
+
}
|
|
1212
|
+
if (kind === 'construct' || kind === 'describe') {
|
|
1213
|
+
const result = await client.constructWorkspaceRaw(modelUri, sparql);
|
|
1214
|
+
return { kind, result };
|
|
1215
|
+
}
|
|
1216
|
+
throw new RestHttpError(400, 'Unsupported or unknown SPARQL query kind.');
|
|
1217
|
+
}
|
|
1218
|
+
function writeSparqlSelectJsonResponse(res, rows) {
|
|
1219
|
+
const vars = new Set();
|
|
1220
|
+
const bindings = rows.map((row) => {
|
|
1221
|
+
const entry = {};
|
|
1222
|
+
for (const [name, term] of row.entries()) {
|
|
1223
|
+
vars.add(name);
|
|
1224
|
+
entry[name] = sparqlJsonBinding(term);
|
|
1225
|
+
}
|
|
1226
|
+
return entry;
|
|
1227
|
+
});
|
|
1228
|
+
const payload = {
|
|
1229
|
+
head: { vars: Array.from(vars) },
|
|
1230
|
+
results: { bindings },
|
|
1231
|
+
};
|
|
1232
|
+
textResponse(res, 200, 'application/sparql-results+json', JSON.stringify(payload));
|
|
1233
|
+
}
|
|
1234
|
+
function writeSparqlAskJsonResponse(res, value) {
|
|
1235
|
+
const payload = {
|
|
1236
|
+
head: {},
|
|
1237
|
+
boolean: value,
|
|
1238
|
+
};
|
|
1239
|
+
textResponse(res, 200, 'application/sparql-results+json', JSON.stringify(payload));
|
|
952
1240
|
}
|
|
953
1241
|
function withTimeout(promise, timeoutMs, method) {
|
|
954
1242
|
return new Promise((resolve, reject) => {
|
|
@@ -1506,9 +1794,28 @@ class InMemoryJsonRpcLspClient {
|
|
|
1506
1794
|
};
|
|
1507
1795
|
return await lintWorkspace(context, params);
|
|
1508
1796
|
}
|
|
1509
|
-
async
|
|
1797
|
+
async queryWorkspaceRaw(modelUri, sparql) {
|
|
1510
1798
|
await this.ensureInitialized();
|
|
1511
1799
|
await this.ensureWorkspaceCurrent();
|
|
1800
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1801
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1802
|
+
return await reasoningService.getSparqlService().query(modelUri, sparql);
|
|
1803
|
+
}
|
|
1804
|
+
async askWorkspaceRaw(modelUri, sparql) {
|
|
1805
|
+
await this.ensureInitialized();
|
|
1806
|
+
await this.ensureWorkspaceCurrent();
|
|
1807
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1808
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1809
|
+
return await reasoningService.getSparqlService().ask(modelUri, sparql);
|
|
1810
|
+
}
|
|
1811
|
+
async constructWorkspaceRaw(modelUri, sparql) {
|
|
1812
|
+
await this.ensureInitialized();
|
|
1813
|
+
await this.ensureWorkspaceCurrent();
|
|
1814
|
+
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1815
|
+
await reasoningService.ensureQueryContext(modelUri);
|
|
1816
|
+
return await reasoningService.getSparqlService().construct(modelUri, sparql);
|
|
1817
|
+
}
|
|
1818
|
+
async queryWorkspace(params) {
|
|
1512
1819
|
const modelUri = typeof params.modelUri === 'string' ? params.modelUri.trim() : '';
|
|
1513
1820
|
const sparql = typeof params.sparql === 'string' ? params.sparql : '';
|
|
1514
1821
|
if (!modelUri) {
|
|
@@ -1520,12 +1827,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
1520
1827
|
};
|
|
1521
1828
|
}
|
|
1522
1829
|
try {
|
|
1523
|
-
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1524
1830
|
const kind = detectSparqlKind(sparql);
|
|
1525
|
-
await reasoningService.ensureQueryContext(modelUri);
|
|
1526
|
-
const sparqlService = reasoningService.getSparqlService();
|
|
1527
1831
|
if (kind === 'select') {
|
|
1528
|
-
const result = await
|
|
1832
|
+
const result = await this.queryWorkspaceRaw(modelUri, sparql);
|
|
1529
1833
|
return {
|
|
1530
1834
|
success: result.success,
|
|
1531
1835
|
kind,
|
|
@@ -1535,7 +1839,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1535
1839
|
};
|
|
1536
1840
|
}
|
|
1537
1841
|
if (kind === 'ask') {
|
|
1538
|
-
const result = await
|
|
1842
|
+
const result = await this.askWorkspaceRaw(modelUri, sparql);
|
|
1539
1843
|
return {
|
|
1540
1844
|
success: result.success,
|
|
1541
1845
|
kind,
|
|
@@ -1545,7 +1849,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1545
1849
|
};
|
|
1546
1850
|
}
|
|
1547
1851
|
if (kind === 'construct' || kind === 'describe') {
|
|
1548
|
-
const result = await
|
|
1852
|
+
const result = await this.constructWorkspaceRaw(modelUri, sparql);
|
|
1549
1853
|
return {
|
|
1550
1854
|
success: result.success,
|
|
1551
1855
|
kind,
|
|
@@ -2172,12 +2476,12 @@ class InMemoryJsonRpcLspClient {
|
|
|
2172
2476
|
export async function startOmlRestServer(options) {
|
|
2173
2477
|
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2174
2478
|
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.watchWorkspace === true, options.runtime);
|
|
2175
|
-
|
|
2479
|
+
let openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2176
2480
|
let currentAccessToken = options.authToken;
|
|
2177
2481
|
const featureGate = options.featureGate;
|
|
2178
2482
|
if (featureGate && currentAccessToken) {
|
|
2179
2483
|
featureGate.setAccessToken(currentAccessToken);
|
|
2180
|
-
|
|
2484
|
+
void featureGate.primeEntitlements().catch(() => undefined);
|
|
2181
2485
|
}
|
|
2182
2486
|
const getAccessToken = () => currentAccessToken;
|
|
2183
2487
|
const server = http.createServer(async (req, res) => {
|
|
@@ -2206,14 +2510,82 @@ export async function startOmlRestServer(options) {
|
|
|
2206
2510
|
jsonResponse(res, 200, openApiSpec);
|
|
2207
2511
|
return;
|
|
2208
2512
|
}
|
|
2513
|
+
if (method === 'GET' && (pathname === '/docs' || pathname === '/docs/')) {
|
|
2514
|
+
htmlResponse(res, 200, createSwaggerUiPage());
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (method === 'GET' && pathname.startsWith('/docs/')) {
|
|
2518
|
+
const assetName = decodeURIComponent(pathname.slice('/docs/'.length)).trim();
|
|
2519
|
+
if (!assetName || assetName.includes('/') || assetName.includes('\\')) {
|
|
2520
|
+
jsonResponse(res, 404, { error: 'Swagger asset not found.', requestId });
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
await serveSwaggerAsset(res, assetName);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2209
2526
|
if (method === 'GET' && pathname === '/v0/models') {
|
|
2210
|
-
const
|
|
2527
|
+
const requiredFeature = requiredFeatureForRestOperation('models');
|
|
2528
|
+
const workspaceModelFiles = (featureGate && requiredFeature)
|
|
2529
|
+
? await featureGate.runWithFeature(requiredFeature, () => listWorkspaceModelFiles(workspaceRoot), { transport: 'rest', operationId: 'models' })
|
|
2530
|
+
: await listWorkspaceModelFiles(workspaceRoot);
|
|
2211
2531
|
jsonResponse(res, 200, {
|
|
2212
2532
|
files: workspaceModelFiles,
|
|
2213
2533
|
requestId,
|
|
2214
2534
|
});
|
|
2215
2535
|
return;
|
|
2216
2536
|
}
|
|
2537
|
+
if ((method === 'GET' || method === 'POST') && pathname.startsWith('/v0/sparql/')) {
|
|
2538
|
+
const runSparql = async () => {
|
|
2539
|
+
const routeModelPath = resolveSparqlModelPathFromRoute(pathname);
|
|
2540
|
+
if (!routeModelPath) {
|
|
2541
|
+
throw new RestHttpError(404, `No route for ${method} ${pathname}.`);
|
|
2542
|
+
}
|
|
2543
|
+
const modelUri = resolveModelUriFromRouteModelPath(workspaceRoot, routeModelPath);
|
|
2544
|
+
try {
|
|
2545
|
+
await assertModelUriExists(modelUri);
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
throw new RestHttpError(404, `No OML model found for '${routeModelPath}'.`);
|
|
2549
|
+
}
|
|
2550
|
+
const bodyText = method === 'POST' ? await readTextBody(req) : '';
|
|
2551
|
+
const sparql = parseSparqlQueryFromRequest(req, parsed, bodyText);
|
|
2552
|
+
if (!sparql) {
|
|
2553
|
+
throw new RestHttpError(400, "Missing SPARQL query. Provide 'query' in URL params or request body.");
|
|
2554
|
+
}
|
|
2555
|
+
const queryResult = await executeSparqlProtocolQuery(client, modelUri, sparql);
|
|
2556
|
+
if (!queryResult.result.success) {
|
|
2557
|
+
throw new RestHttpError(400, queryResult.result.error || 'SPARQL query failed.');
|
|
2558
|
+
}
|
|
2559
|
+
if (queryResult.kind === 'select') {
|
|
2560
|
+
if (!selectAcceptsSparqlJson(req.headers.accept)) {
|
|
2561
|
+
throw new RestHttpError(406, 'Not Acceptable for SELECT results.');
|
|
2562
|
+
}
|
|
2563
|
+
writeSparqlSelectJsonResponse(res, queryResult.result.rows);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
if (queryResult.kind === 'ask') {
|
|
2567
|
+
if (!selectAcceptsSparqlJson(req.headers.accept)) {
|
|
2568
|
+
throw new RestHttpError(406, 'Not Acceptable for ASK results.');
|
|
2569
|
+
}
|
|
2570
|
+
writeSparqlAskJsonResponse(res, queryResult.result.result);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const graphFormat = selectGraphResponseFormat(req.headers.accept);
|
|
2574
|
+
if (!graphFormat) {
|
|
2575
|
+
throw new RestHttpError(406, 'Not Acceptable for graph results.');
|
|
2576
|
+
}
|
|
2577
|
+
const serialized = await serializeQuads(queryResult.result.quads, graphFormat.format, false);
|
|
2578
|
+
textResponse(res, 200, graphFormat.contentType, serialized);
|
|
2579
|
+
};
|
|
2580
|
+
const requiredFeature = requiredFeatureForRestOperation('query');
|
|
2581
|
+
if (featureGate && requiredFeature) {
|
|
2582
|
+
await featureGate.runWithFeature(requiredFeature, runSparql, { transport: 'rest', operationId: 'sparql' });
|
|
2583
|
+
}
|
|
2584
|
+
else {
|
|
2585
|
+
await runSparql();
|
|
2586
|
+
}
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2217
2589
|
if (method === 'POST') {
|
|
2218
2590
|
const body = await readJsonBody(req);
|
|
2219
2591
|
const operationId = resolveRestOperationId(method, pathname);
|
|
@@ -2235,9 +2607,15 @@ export async function startOmlRestServer(options) {
|
|
|
2235
2607
|
}
|
|
2236
2608
|
catch (error) {
|
|
2237
2609
|
const message = error instanceof Error ? error.message : String(error);
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2610
|
+
if (error instanceof OmlAccessError) {
|
|
2611
|
+
jsonResponse(res, error.statusCode, accessErrorPayload(error, requestId));
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
if (error instanceof RestHttpError) {
|
|
2615
|
+
jsonResponse(res, error.statusCode, { error: message, requestId });
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const status = error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500;
|
|
2241
2619
|
jsonResponse(res, status, { error: message, requestId });
|
|
2242
2620
|
}
|
|
2243
2621
|
});
|
|
@@ -2250,6 +2628,8 @@ export async function startOmlRestServer(options) {
|
|
|
2250
2628
|
server.once('error', reject);
|
|
2251
2629
|
server.listen(options.port, options.host, () => resolve());
|
|
2252
2630
|
});
|
|
2631
|
+
const listeningPort = resolveListeningPort(server);
|
|
2632
|
+
openApiSpec = createOpenApiSpec(options.host, listeningPort);
|
|
2253
2633
|
return {
|
|
2254
2634
|
server,
|
|
2255
2635
|
updateToken: async (token) => {
|