@oml/server 0.14.1 → 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 +27 -0
- package/out/auth/feature-gate.js +247 -0
- package/out/auth/feature-gate.js.map +1 -0
- package/out/auth/feature-policy.d.ts +14 -0
- package/out/auth/feature-policy.js +47 -0
- package/out/auth/feature-policy.js.map +1 -0
- package/out/cli.js +79 -18
- 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 +3 -1
- package/out/lsp/language-server.js +54 -0
- 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 +2 -9
- package/out/rest/routes.js +406 -15
- package/out/rest/routes.js.map +1 -1
- package/out/rest/server.d.ts +3 -1
- package/out/rest/server.js +465 -66
- 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';
|
|
@@ -18,13 +18,28 @@ import { applyOmlUpdate, collectOntologyMembers, getIriForNode, getOntologyModel
|
|
|
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
|
-
import { createOpenApiSpec,
|
|
21
|
+
import { createOpenApiSpec, dispatchRestOperation, resolveRestOperationId } from './routes.js';
|
|
22
22
|
import { buildTemplateCatalog, expandTemplateComposeBlocks, findFilesByExtension, frontMatterString, isTemplateMarkdownFile, normalizeContextOntologyIri, } from './template.js';
|
|
23
23
|
import { lintWorkspace, reasonWorkspace, validateWorkspace } from './validation.js';
|
|
24
|
+
import { OmlAccessError, requiredFeatureForRestOperation, } from '../auth/feature-policy.js';
|
|
24
25
|
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
25
26
|
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
26
27
|
const DEFAULT_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
27
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;
|
|
28
43
|
const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
29
44
|
'table',
|
|
30
45
|
'tree',
|
|
@@ -36,6 +51,13 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set([
|
|
|
36
51
|
'matrix',
|
|
37
52
|
'table-editor'
|
|
38
53
|
]);
|
|
54
|
+
class RestHttpError extends Error {
|
|
55
|
+
constructor(statusCode, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'RestHttpError';
|
|
58
|
+
this.statusCode = statusCode;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
39
61
|
const BUILT_IN_ONTOLOGIES = new Set([
|
|
40
62
|
'http://www.w3.org/2001/XMLSchema',
|
|
41
63
|
'http://www.w3.org/1999/02/22-rdf-syntax-ns',
|
|
@@ -51,6 +73,79 @@ function jsonResponse(res, status, payload) {
|
|
|
51
73
|
res.setHeader('content-length', Buffer.byteLength(body, 'utf-8'));
|
|
52
74
|
res.end(body);
|
|
53
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
|
+
}
|
|
54
149
|
function htmlResponse(res, status, body) {
|
|
55
150
|
res.statusCode = status;
|
|
56
151
|
res.setHeader('content-type', HTML_CONTENT_TYPE);
|
|
@@ -153,6 +248,14 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
153
248
|
padding: 20px 24px;
|
|
154
249
|
}
|
|
155
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
|
+
|
|
156
259
|
.page-title {
|
|
157
260
|
margin: 0;
|
|
158
261
|
font-size: clamp(1.9rem, 3.4vw, 2.7rem);
|
|
@@ -167,6 +270,34 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
167
270
|
font-size: 0.98rem;
|
|
168
271
|
}
|
|
169
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
|
+
|
|
170
301
|
.row {
|
|
171
302
|
background: var(--card);
|
|
172
303
|
border: 1px solid var(--line-soft);
|
|
@@ -555,7 +686,15 @@ function createSparqlWorkbenchPage(defaultWorkspaceRoot) {
|
|
|
555
686
|
<body>
|
|
556
687
|
<div class="shell">
|
|
557
688
|
<section class="hero">
|
|
558
|
-
<
|
|
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>
|
|
559
698
|
<p class="page-subtitle">Run SPARQL queries against workspace models with fast browsing, paged table results, and raw JSON diagnostics.</p>
|
|
560
699
|
</section>
|
|
561
700
|
|
|
@@ -659,7 +798,7 @@ LIMIT 25</textarea>
|
|
|
659
798
|
const browseFilter = document.getElementById('browseFilter');
|
|
660
799
|
|
|
661
800
|
let browseEntries = [];
|
|
662
|
-
let
|
|
801
|
+
let selectedModelId = '';
|
|
663
802
|
let currentTableColumns = [];
|
|
664
803
|
let currentTableRows = [];
|
|
665
804
|
let currentPageIndex = 0;
|
|
@@ -692,8 +831,8 @@ LIMIT 25</textarea>
|
|
|
692
831
|
if (!term || typeof term !== 'object') {
|
|
693
832
|
return '';
|
|
694
833
|
}
|
|
695
|
-
if (term.
|
|
696
|
-
const suffix = term
|
|
834
|
+
if (term.type === 'literal') {
|
|
835
|
+
const suffix = term['xml:lang'] ? '@' + term['xml:lang'] : (term.datatype ? '^^' + term.datatype : '');
|
|
697
836
|
return '"' + term.value + '"' + suffix;
|
|
698
837
|
}
|
|
699
838
|
return String(term.value ?? '');
|
|
@@ -737,14 +876,15 @@ LIMIT 25</textarea>
|
|
|
737
876
|
setTableStatus('');
|
|
738
877
|
}
|
|
739
878
|
|
|
740
|
-
function
|
|
741
|
-
const
|
|
742
|
-
|
|
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) {
|
|
743
883
|
resetTable('The SELECT query returned no rows.');
|
|
744
884
|
return;
|
|
745
885
|
}
|
|
746
|
-
currentTableColumns = [...new Set(
|
|
747
|
-
currentTableRows =
|
|
886
|
+
currentTableColumns = vars.length > 0 ? vars : [...new Set(bindings.flatMap((row) => Object.keys(row)))];
|
|
887
|
+
currentTableRows = bindings;
|
|
748
888
|
currentPageIndex = 0;
|
|
749
889
|
renderTablePage();
|
|
750
890
|
}
|
|
@@ -755,9 +895,11 @@ LIMIT 25</textarea>
|
|
|
755
895
|
return;
|
|
756
896
|
}
|
|
757
897
|
browseList.innerHTML = entries.map((entry) => {
|
|
758
|
-
const
|
|
898
|
+
const normalizedPath = String(entry.path || '').trim();
|
|
899
|
+
const modelId = normalizedPath.replace(/\.oml$/i, '');
|
|
900
|
+
const isSelected = selectedModelId && selectedModelId === modelId;
|
|
759
901
|
const selectedClass = isSelected ? ' selected' : '';
|
|
760
|
-
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) + '">'
|
|
761
903
|
+ '<div class="browse-entry-path" title="' + escapeClientHtml(entry.path) + '">' + escapeClientHtml(entry.path) + '</div>'
|
|
762
904
|
+ '</button>';
|
|
763
905
|
}).join('');
|
|
@@ -792,29 +934,23 @@ LIMIT 25</textarea>
|
|
|
792
934
|
}
|
|
793
935
|
}
|
|
794
936
|
|
|
795
|
-
function
|
|
796
|
-
if (!
|
|
937
|
+
function validateModelId(modelId) {
|
|
938
|
+
if (!modelId) {
|
|
797
939
|
return 'OML file is required.';
|
|
798
940
|
}
|
|
799
|
-
if (
|
|
800
|
-
return '
|
|
801
|
-
}
|
|
802
|
-
if (modelUri.toLowerCase().endsWith('.md')) {
|
|
803
|
-
return 'Markdown files are not queryable directly. Choose an .oml file or resolve the markdown document\\'s ontology context first.';
|
|
804
|
-
}
|
|
805
|
-
if (!modelUri.toLowerCase().endsWith('.oml')) {
|
|
806
|
-
return 'Choose an .oml model file.';
|
|
941
|
+
if (modelId.toLowerCase().endsWith('.md')) {
|
|
942
|
+
return 'Markdown files are not queryable directly. Choose an .oml file.';
|
|
807
943
|
}
|
|
808
944
|
return undefined;
|
|
809
945
|
}
|
|
810
946
|
|
|
811
947
|
async function executeQuery() {
|
|
812
|
-
const
|
|
948
|
+
const modelId = selectedModelId.trim();
|
|
813
949
|
const sparql = sparqlInput.value.trim();
|
|
814
|
-
const
|
|
815
|
-
if (
|
|
816
|
-
setStatus(
|
|
817
|
-
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);
|
|
818
954
|
resetTable('Choose a valid OML model file to run a query.');
|
|
819
955
|
browseModelsButton.focus();
|
|
820
956
|
return;
|
|
@@ -831,36 +967,50 @@ LIMIT 25</textarea>
|
|
|
831
967
|
setStatus('Running query...', '');
|
|
832
968
|
|
|
833
969
|
try {
|
|
834
|
-
const response = await fetch('/v0/
|
|
970
|
+
const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
|
|
835
971
|
method: 'POST',
|
|
836
|
-
headers: {
|
|
837
|
-
|
|
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
|
|
838
977
|
});
|
|
839
|
-
const
|
|
840
|
-
|
|
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;
|
|
841
989
|
|
|
842
990
|
if (!response.ok) {
|
|
843
991
|
resetTable('Raw response is shown below.');
|
|
844
|
-
|
|
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');
|
|
845
1002
|
return;
|
|
846
1003
|
}
|
|
847
1004
|
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
if (
|
|
851
|
-
|
|
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);
|
|
852
1009
|
} else {
|
|
853
1010
|
resetTable('Raw response is shown below. Table rendering is available only for SELECT queries.');
|
|
854
1011
|
}
|
|
855
1012
|
|
|
856
|
-
|
|
857
|
-
setStatus(result.error || 'Query completed with an error.', 'error');
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
const warnings = Array.isArray(result && result.warnings) ? result.warnings.length : 0;
|
|
862
|
-
const warningSuffix = warnings > 0 ? ' ' + warnings + ' warning(s).' : '';
|
|
863
|
-
setStatus('Query completed as ' + kind.toUpperCase() + '.' + warningSuffix, 'success');
|
|
1013
|
+
setStatus('Query completed.', 'success');
|
|
864
1014
|
} catch (error) {
|
|
865
1015
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
866
1016
|
resetTable('Raw response is shown below.');
|
|
@@ -884,16 +1034,16 @@ LIMIT 25</textarea>
|
|
|
884
1034
|
});
|
|
885
1035
|
|
|
886
1036
|
browseList.addEventListener('click', (event) => {
|
|
887
|
-
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;
|
|
888
1038
|
if (!target) {
|
|
889
1039
|
return;
|
|
890
1040
|
}
|
|
891
|
-
const
|
|
1041
|
+
const modelId = target.getAttribute('data-model-id');
|
|
892
1042
|
const relativePath = target.getAttribute('data-path');
|
|
893
|
-
if (!
|
|
1043
|
+
if (!modelId) {
|
|
894
1044
|
return;
|
|
895
1045
|
}
|
|
896
|
-
|
|
1046
|
+
selectedModelId = modelId;
|
|
897
1047
|
modelUriInput.value = relativePath || '';
|
|
898
1048
|
setStatus('Selected workspace OML file.', 'success');
|
|
899
1049
|
applyBrowseFilter();
|
|
@@ -933,6 +1083,14 @@ LIMIT 25</textarea>
|
|
|
933
1083
|
</html>`;
|
|
934
1084
|
}
|
|
935
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) {
|
|
936
1094
|
const chunks = [];
|
|
937
1095
|
let total = 0;
|
|
938
1096
|
for await (const chunk of req) {
|
|
@@ -944,10 +1102,141 @@ async function readJsonBody(req) {
|
|
|
944
1102
|
chunks.push(buffer);
|
|
945
1103
|
}
|
|
946
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') {
|
|
947
1161
|
return {};
|
|
948
1162
|
}
|
|
949
|
-
|
|
950
|
-
|
|
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));
|
|
951
1240
|
}
|
|
952
1241
|
function withTimeout(promise, timeoutMs, method) {
|
|
953
1242
|
return new Promise((resolve, reject) => {
|
|
@@ -1505,9 +1794,28 @@ class InMemoryJsonRpcLspClient {
|
|
|
1505
1794
|
};
|
|
1506
1795
|
return await lintWorkspace(context, params);
|
|
1507
1796
|
}
|
|
1508
|
-
async
|
|
1797
|
+
async queryWorkspaceRaw(modelUri, sparql) {
|
|
1798
|
+
await this.ensureInitialized();
|
|
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) {
|
|
1509
1812
|
await this.ensureInitialized();
|
|
1510
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) {
|
|
1511
1819
|
const modelUri = typeof params.modelUri === 'string' ? params.modelUri.trim() : '';
|
|
1512
1820
|
const sparql = typeof params.sparql === 'string' ? params.sparql : '';
|
|
1513
1821
|
if (!modelUri) {
|
|
@@ -1519,12 +1827,9 @@ class InMemoryJsonRpcLspClient {
|
|
|
1519
1827
|
};
|
|
1520
1828
|
}
|
|
1521
1829
|
try {
|
|
1522
|
-
const reasoningService = this.runtime.Oml.reasoning.ReasoningService;
|
|
1523
1830
|
const kind = detectSparqlKind(sparql);
|
|
1524
|
-
await reasoningService.ensureQueryContext(modelUri);
|
|
1525
|
-
const sparqlService = reasoningService.getSparqlService();
|
|
1526
1831
|
if (kind === 'select') {
|
|
1527
|
-
const result = await
|
|
1832
|
+
const result = await this.queryWorkspaceRaw(modelUri, sparql);
|
|
1528
1833
|
return {
|
|
1529
1834
|
success: result.success,
|
|
1530
1835
|
kind,
|
|
@@ -1534,7 +1839,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1534
1839
|
};
|
|
1535
1840
|
}
|
|
1536
1841
|
if (kind === 'ask') {
|
|
1537
|
-
const result = await
|
|
1842
|
+
const result = await this.askWorkspaceRaw(modelUri, sparql);
|
|
1538
1843
|
return {
|
|
1539
1844
|
success: result.success,
|
|
1540
1845
|
kind,
|
|
@@ -1544,7 +1849,7 @@ class InMemoryJsonRpcLspClient {
|
|
|
1544
1849
|
};
|
|
1545
1850
|
}
|
|
1546
1851
|
if (kind === 'construct' || kind === 'describe') {
|
|
1547
|
-
const result = await
|
|
1852
|
+
const result = await this.constructWorkspaceRaw(modelUri, sparql);
|
|
1548
1853
|
return {
|
|
1549
1854
|
success: result.success,
|
|
1550
1855
|
kind,
|
|
@@ -2171,8 +2476,13 @@ class InMemoryJsonRpcLspClient {
|
|
|
2171
2476
|
export async function startOmlRestServer(options) {
|
|
2172
2477
|
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
|
|
2173
2478
|
const client = new InMemoryJsonRpcLspClient(workspaceRoot, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options.watchWorkspace === true, options.runtime);
|
|
2174
|
-
|
|
2479
|
+
let openApiSpec = createOpenApiSpec(options.host, options.port);
|
|
2175
2480
|
let currentAccessToken = options.authToken;
|
|
2481
|
+
const featureGate = options.featureGate;
|
|
2482
|
+
if (featureGate && currentAccessToken) {
|
|
2483
|
+
featureGate.setAccessToken(currentAccessToken);
|
|
2484
|
+
void featureGate.primeEntitlements().catch(() => undefined);
|
|
2485
|
+
}
|
|
2176
2486
|
const getAccessToken = () => currentAccessToken;
|
|
2177
2487
|
const server = http.createServer(async (req, res) => {
|
|
2178
2488
|
const requestId = randomUUID();
|
|
@@ -2200,22 +2510,94 @@ export async function startOmlRestServer(options) {
|
|
|
2200
2510
|
jsonResponse(res, 200, openApiSpec);
|
|
2201
2511
|
return;
|
|
2202
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
|
+
}
|
|
2203
2526
|
if (method === 'GET' && pathname === '/v0/models') {
|
|
2204
|
-
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);
|
|
2205
2531
|
jsonResponse(res, 200, {
|
|
2206
2532
|
files: workspaceModelFiles,
|
|
2207
2533
|
requestId,
|
|
2208
2534
|
});
|
|
2209
2535
|
return;
|
|
2210
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
|
+
}
|
|
2211
2589
|
if (method === 'POST') {
|
|
2212
2590
|
const body = await readJsonBody(req);
|
|
2213
|
-
const
|
|
2214
|
-
if (
|
|
2591
|
+
const operationId = resolveRestOperationId(method, pathname);
|
|
2592
|
+
if (operationId) {
|
|
2593
|
+
const requiredFeature = requiredFeatureForRestOperation(operationId);
|
|
2594
|
+
const result = (featureGate && requiredFeature)
|
|
2595
|
+
? await featureGate.runWithFeature(requiredFeature, () => dispatchRestOperation(operationId, body, client), { transport: 'rest', operationId })
|
|
2596
|
+
: await dispatchRestOperation(operationId, body, client);
|
|
2215
2597
|
jsonResponse(res, 200, {
|
|
2216
2598
|
ok: true,
|
|
2217
|
-
method: `oml/rest/${
|
|
2218
|
-
result
|
|
2599
|
+
method: `oml/rest/${operationId}`,
|
|
2600
|
+
result,
|
|
2219
2601
|
requestId
|
|
2220
2602
|
});
|
|
2221
2603
|
return;
|
|
@@ -2225,21 +2607,38 @@ export async function startOmlRestServer(options) {
|
|
|
2225
2607
|
}
|
|
2226
2608
|
catch (error) {
|
|
2227
2609
|
const message = error instanceof Error ? error.message : String(error);
|
|
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
|
+
}
|
|
2228
2618
|
const status = error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500;
|
|
2229
2619
|
jsonResponse(res, status, { error: message, requestId });
|
|
2230
2620
|
}
|
|
2231
2621
|
});
|
|
2232
2622
|
server.on('close', () => {
|
|
2233
2623
|
void client.close();
|
|
2624
|
+
void featureGate?.dispose();
|
|
2234
2625
|
currentAccessToken = undefined;
|
|
2235
2626
|
});
|
|
2236
2627
|
await new Promise((resolve, reject) => {
|
|
2237
2628
|
server.once('error', reject);
|
|
2238
2629
|
server.listen(options.port, options.host, () => resolve());
|
|
2239
2630
|
});
|
|
2631
|
+
const listeningPort = resolveListeningPort(server);
|
|
2632
|
+
openApiSpec = createOpenApiSpec(options.host, listeningPort);
|
|
2240
2633
|
return {
|
|
2241
2634
|
server,
|
|
2242
|
-
updateToken: (token) => {
|
|
2635
|
+
updateToken: async (token) => {
|
|
2636
|
+
currentAccessToken = token;
|
|
2637
|
+
featureGate?.setAccessToken(token);
|
|
2638
|
+
if (featureGate) {
|
|
2639
|
+
await featureGate.primeEntitlements();
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2243
2642
|
};
|
|
2244
2643
|
}
|
|
2245
2644
|
//# sourceMappingURL=server.js.map
|