@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.
@@ -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, dispatchRestRoute } from './routes.js';
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
- <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>
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 selectedModelUri = '';
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.termType === 'Literal') {
696
- 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 : '');
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 renderSelectTable(result) {
741
- const rows = Array.isArray(result.rows) ? result.rows : [];
742
- 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) {
743
883
  resetTable('The SELECT query returned no rows.');
744
884
  return;
745
885
  }
746
- currentTableColumns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
747
- currentTableRows = rows;
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 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;
759
901
  const selectedClass = isSelected ? ' selected' : '';
760
- 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) + '">'
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 validateModelUri(modelUri) {
796
- if (!modelUri) {
937
+ function validateModelId(modelId) {
938
+ if (!modelId) {
797
939
  return 'OML file is required.';
798
940
  }
799
- if (!modelUri.startsWith('file://')) {
800
- return 'OML file must be a file:// URI for an .oml file under the server workspace.';
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 modelUri = selectedModelUri.trim();
948
+ const modelId = selectedModelId.trim();
813
949
  const sparql = sparqlInput.value.trim();
814
- const modelUriError = validateModelUri(modelUri);
815
- if (modelUriError) {
816
- setStatus(modelUriError, 'error');
817
- 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);
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/query', {
970
+ const response = await fetch('/v0/sparql/' + encodeURIComponent(modelId), {
835
971
  method: 'POST',
836
- headers: { 'content-type': 'application/json' },
837
- 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
838
977
  });
839
- const payload = await response.json();
840
- 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;
841
989
 
842
990
  if (!response.ok) {
843
991
  resetTable('Raw response is shown below.');
844
- 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');
845
1002
  return;
846
1003
  }
847
1004
 
848
- const result = payload && typeof payload === 'object' ? payload.result : undefined;
849
- const kind = result && typeof result.kind === 'string' ? result.kind : 'unknown';
850
- if (kind === 'select') {
851
- 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);
852
1009
  } else {
853
1010
  resetTable('Raw response is shown below. Table rendering is available only for SELECT queries.');
854
1011
  }
855
1012
 
856
- if (result && result.success === false) {
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-uri]') : null;
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 uri = target.getAttribute('data-uri');
1041
+ const modelId = target.getAttribute('data-model-id');
892
1042
  const relativePath = target.getAttribute('data-path');
893
- if (!uri) {
1043
+ if (!modelId) {
894
1044
  return;
895
1045
  }
896
- selectedModelUri = uri;
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
- const parsed = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
950
- 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));
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 queryWorkspace(params) {
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 sparqlService.query(modelUri, sparql);
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 sparqlService.ask(modelUri, sparql);
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 sparqlService.construct(modelUri, sparql);
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
- const openApiSpec = createOpenApiSpec(options.host, options.port);
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 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);
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 route = await dispatchRestRoute(method, pathname, body, client);
2214
- if (route) {
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/${route.operationId}`,
2218
- result: route.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) => { currentAccessToken = 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