@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.
@@ -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
- <h1 class="page-title">OML Language Server</h1>
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 selectedModelUri = '';
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.termType === 'Literal') {
697
- const suffix = term.language ? '@' + term.language : (term.datatype ? '^^' + term.datatype : '');
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 renderSelectTable(result) {
742
- const rows = Array.isArray(result.rows) ? result.rows : [];
743
- if (rows.length === 0) {
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(rows.flatMap((row) => Object.keys(row)))];
748
- currentTableRows = rows;
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 isSelected = selectedModelUri && selectedModelUri === entry.uri;
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-uri="' + escapeClientHtml(entry.uri) + '" data-path="' + escapeClientHtml(entry.path) + '">'
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 validateModelUri(modelUri) {
797
- if (!modelUri) {
952
+ function validateModelId(modelId) {
953
+ if (!modelId) {
798
954
  return 'OML file is required.';
799
955
  }
800
- if (!modelUri.startsWith('file://')) {
801
- return 'OML file must be a file:// URI for an .oml file under the server workspace.';
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 modelUri = selectedModelUri.trim();
963
+ const modelId = selectedModelId.trim();
814
964
  const sparql = sparqlInput.value.trim();
815
- const modelUriError = validateModelUri(modelUri);
816
- if (modelUriError) {
817
- setStatus(modelUriError, 'error');
818
- rawOutput.textContent = JSON.stringify({ error: modelUriError }, null, 2);
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/query', {
985
+ const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
836
986
  method: 'POST',
837
- headers: { 'content-type': 'application/json' },
838
- body: JSON.stringify({ modelUri, sparql })
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 payload = await response.json();
841
- rawOutput.textContent = JSON.stringify(payload, null, 2);
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
- setStatus(payload.error || 'Request failed.', 'error');
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 result = payload && typeof payload === 'object' ? payload.result : undefined;
850
- const kind = result && typeof result.kind === 'string' ? result.kind : 'unknown';
851
- if (kind === 'select') {
852
- renderSelectTable(result);
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
- if (result && result.success === false) {
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-uri]') : null;
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 uri = target.getAttribute('data-uri');
1056
+ const modelId = target.getAttribute('data-model-id');
893
1057
  const relativePath = target.getAttribute('data-path');
894
- if (!uri) {
1058
+ if (!modelId) {
895
1059
  return;
896
1060
  }
897
- selectedModelUri = uri;
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
- const parsed = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
951
- return typeof parsed === 'object' && parsed !== null ? parsed : {};
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 queryWorkspace(params) {
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 sparqlService.query(modelUri, sparql);
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 sparqlService.ask(modelUri, sparql);
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 sparqlService.construct(modelUri, sparql);
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
- const openApiSpec = createOpenApiSpec(options.host, options.port);
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
- await featureGate.primeEntitlements();
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 workspaceModelFiles = await listWorkspaceModelFiles(workspaceRoot);
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
- const status = error instanceof OmlAccessError
2239
- ? error.statusCode
2240
- : (error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500);
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) => {