@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.
@@ -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
- <h1 class="page-title">OML Language Server</h1>
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 selectedModelUri = '';
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.termType === 'Literal') {
697
- const suffix = term.language ? '@' + term.language : (term.datatype ? '^^' + term.datatype : '');
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 renderSelectTable(result) {
742
- const rows = Array.isArray(result.rows) ? result.rows : [];
743
- if (rows.length === 0) {
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(rows.flatMap((row) => Object.keys(row)))];
748
- currentTableRows = rows;
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 isSelected = selectedModelUri && selectedModelUri === entry.uri;
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-uri="' + escapeClientHtml(entry.uri) + '" data-path="' + escapeClientHtml(entry.path) + '">'
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 validateModelUri(modelUri) {
797
- if (!modelUri) {
937
+ function validateModelId(modelId) {
938
+ if (!modelId) {
798
939
  return 'OML file is required.';
799
940
  }
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.';
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 modelUri = selectedModelUri.trim();
948
+ const modelId = selectedModelId.trim();
814
949
  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);
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/query', {
970
+ const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
836
971
  method: 'POST',
837
- headers: { 'content-type': 'application/json' },
838
- body: JSON.stringify({ modelUri, sparql })
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 payload = await response.json();
841
- rawOutput.textContent = JSON.stringify(payload, null, 2);
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
- setStatus(payload.error || 'Request failed.', 'error');
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 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);
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
- 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');
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-uri]') : null;
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 uri = target.getAttribute('data-uri');
1041
+ const modelId = target.getAttribute('data-model-id');
893
1042
  const relativePath = target.getAttribute('data-path');
894
- if (!uri) {
1043
+ if (!modelId) {
895
1044
  return;
896
1045
  }
897
- selectedModelUri = uri;
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
- const parsed = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
951
- return typeof parsed === 'object' && parsed !== null ? parsed : {};
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 queryWorkspace(params) {
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 sparqlService.query(modelUri, sparql);
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 sparqlService.ask(modelUri, sparql);
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 sparqlService.construct(modelUri, sparql);
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
- const openApiSpec = createOpenApiSpec(options.host, options.port);
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
- await featureGate.primeEntitlements();
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 workspaceModelFiles = await listWorkspaceModelFiles(workspaceRoot);
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
- const status = error instanceof OmlAccessError
2239
- ? error.statusCode
2240
- : (error instanceof SyntaxError || message.includes('Request body exceeds') ? 400 : 500);
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) => {