@oml/server 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/package.json +34 -0
- package/src/cli.ts +189 -0
- package/src/index.ts +9 -0
- package/src/lsp/diagram-server.ts +48 -0
- package/src/lsp/language-server.ts +423 -0
- package/src/lsp/protocol/browser-fs-protocol.ts +21 -0
- package/src/lsp/protocol/reasoner-protocol.ts +86 -0
- package/src/lsp/providers/browser-fs-provider.ts +85 -0
- package/src/lsp/providers/hybrid-fs-provider.ts +134 -0
- package/src/rest/export.ts +118 -0
- package/src/rest/routes.ts +117 -0
- package/src/rest/server.ts +2517 -0
- package/src/rest/template.ts +276 -0
- package/src/rest/validation.ts +555 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!-- Copyright (c) 2026 Modelware. All rights reserved. -->
|
|
2
|
+
|
|
3
|
+
# @oml/server
|
|
4
|
+
|
|
5
|
+
Standalone OML REST server runtime package.
|
|
6
|
+
|
|
7
|
+
The only user-facing CLI is `oml` from `@oml/cli`. Use `oml server start`, `oml server stop`, and `oml server status` to manage the server.
|
|
8
|
+
|
|
9
|
+
## Options
|
|
10
|
+
|
|
11
|
+
- `--host`, `-h`: bind host. Default: `127.0.0.1`
|
|
12
|
+
- `--port`, `-p`: bind port. Default: `8080`
|
|
13
|
+
- `--workspace`, `-w`: workspace root used to initialize the server. Default: current working directory
|
|
14
|
+
- `--token`: bearer token required for REST endpoints (except `/health`)
|
|
15
|
+
- `--refresh-token`: optional refresh token used by server auth refresh
|
|
16
|
+
- `--token-expires-at`: optional access-token expiration time (epoch ms)
|
|
17
|
+
|
|
18
|
+
## Canonical CLI
|
|
19
|
+
|
|
20
|
+
From a local checkout:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node packages/cli/bin/cli.js server start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When installed from npm:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
oml server start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Server management commands:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
oml server start
|
|
36
|
+
oml server status
|
|
37
|
+
oml server stop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If the requested port is already occupied, startup fails immediately with a clear message naming the occupied host and port.
|
|
41
|
+
|
|
42
|
+
## REST API
|
|
43
|
+
|
|
44
|
+
The server exposes only these endpoints:
|
|
45
|
+
|
|
46
|
+
- `GET /health`
|
|
47
|
+
- `GET /openapi.json`
|
|
48
|
+
- `GET /v0/models`
|
|
49
|
+
- `POST /v0/query`
|
|
50
|
+
- `POST /v0/update`
|
|
51
|
+
- `POST /v0/fuzzysearch`
|
|
52
|
+
- `POST /v0/lint`
|
|
53
|
+
- `POST /v0/reason`
|
|
54
|
+
- `POST /v0/validate`
|
|
55
|
+
- `POST /v0/render`
|
|
56
|
+
- `POST /v0/export`
|
|
57
|
+
|
|
58
|
+
Health check:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl http://127.0.0.1:8080/health
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run a SPARQL query:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
curl -X POST http://127.0.0.1:8080/v0/query \
|
|
68
|
+
-H 'content-type: application/json' \
|
|
69
|
+
-d '{
|
|
70
|
+
"modelUri": "file:///absolute/path/to/model.oml",
|
|
71
|
+
"sparql": "SELECT * WHERE { ?s ?p ?o } LIMIT 10"
|
|
72
|
+
}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run update request:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -X POST http://127.0.0.1:8080/v0/update \
|
|
79
|
+
-H 'content-type: application/json' \
|
|
80
|
+
-d '{
|
|
81
|
+
"operations": []
|
|
82
|
+
}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Run fuzzy search:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
curl -X POST http://127.0.0.1:8080/v0/fuzzysearch \
|
|
89
|
+
-H 'content-type: application/json' \
|
|
90
|
+
-d '{
|
|
91
|
+
"text": "firefighter",
|
|
92
|
+
"limit": 10
|
|
93
|
+
}'
|
|
94
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oml/server",
|
|
3
|
+
"description": "Standalone OML language server host",
|
|
4
|
+
"version": "0.14.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./out/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./out/index.js",
|
|
9
|
+
"./lsp/language-server": "./out/lsp/language-server.js",
|
|
10
|
+
"./lsp/diagram-server": "./out/lsp/diagram-server.js",
|
|
11
|
+
"./rest/server": "./out/rest/server.js",
|
|
12
|
+
"./lsp/providers/browser-fs-provider": "./out/lsp/providers/browser-fs-provider.js",
|
|
13
|
+
"./lsp/providers/hybrid-fs-provider": "./out/lsp/providers/hybrid-fs-provider.js",
|
|
14
|
+
"./lsp/protocol/reasoner-protocol": "./out/lsp/protocol/reasoner-protocol.js",
|
|
15
|
+
"./lsp/protocol/browser-fs-protocol": "./out/lsp/protocol/browser-fs-protocol.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "shx rm -fr *.tsbuildinfo out",
|
|
20
|
+
"build": "echo 'No build step'",
|
|
21
|
+
"build:clean": "npm run clean && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@leeoniya/ufuzzy": "^1.0.19",
|
|
25
|
+
"@oml/language": "0.14.0",
|
|
26
|
+
"@oml/markdown": "0.14.0",
|
|
27
|
+
"@oml/owl": "0.14.0",
|
|
28
|
+
"@oml/platform": "^0.5.0",
|
|
29
|
+
"@oml/reasoner": "^0.3.0",
|
|
30
|
+
"langium": "^4.2.1",
|
|
31
|
+
"langium-cli": "^4.2.0",
|
|
32
|
+
"vscode-languageserver": "~9.0.1"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { NodeFileSystem } from 'langium/node';
|
|
8
|
+
import { createConnection } from 'vscode-languageserver/node.js';
|
|
9
|
+
import { startOmlLanguageServer, type OmlLanguageServerRuntime } from './lsp/language-server.js';
|
|
10
|
+
import { startOmlRestServer } from './rest/server.js';
|
|
11
|
+
|
|
12
|
+
interface RemoteOptions {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
workspaceRoot: string;
|
|
16
|
+
token?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ServerLockOwner = 'daemon' | 'extension';
|
|
20
|
+
|
|
21
|
+
function formatStartupError(error: unknown, host: string, port: number): string {
|
|
22
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
23
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
24
|
+
if (code === 'EADDRINUSE') {
|
|
25
|
+
return `Port ${port} is already in use on ${host}.`;
|
|
26
|
+
}
|
|
27
|
+
if (code === 'EACCES') {
|
|
28
|
+
return `Port ${port} on ${host} cannot be used due to insufficient permissions.`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readOptionValue(argv: string[], index: number, aliases: string[]): string {
|
|
35
|
+
const current = argv[index];
|
|
36
|
+
const equalsIndex = current.indexOf('=');
|
|
37
|
+
if (equalsIndex >= 0) {
|
|
38
|
+
return current.slice(equalsIndex + 1);
|
|
39
|
+
}
|
|
40
|
+
const next = argv[index + 1];
|
|
41
|
+
if (next === undefined || next.startsWith('-')) {
|
|
42
|
+
throw new Error(`Missing value for ${aliases.join('/')}.`);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseRemoteOptions(argv: string[]): RemoteOptions {
|
|
48
|
+
let portValue: string | undefined;
|
|
49
|
+
let workspaceValue: string | undefined;
|
|
50
|
+
let tokenValue: string | undefined;
|
|
51
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
52
|
+
const arg = argv[index];
|
|
53
|
+
if (arg === '--node-ipc' || arg.startsWith('--clientProcessId=')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === '--port' || arg === '-p' || arg.startsWith('--port=')) {
|
|
57
|
+
portValue = readOptionValue(argv, index, ['--port', '-p']).trim();
|
|
58
|
+
if (!arg.includes('=')) {
|
|
59
|
+
index += 1;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === '--workspace' || arg === '-w' || arg.startsWith('--workspace=')) {
|
|
64
|
+
workspaceValue = readOptionValue(argv, index, ['--workspace', '-w']).trim();
|
|
65
|
+
if (!arg.includes('=')) {
|
|
66
|
+
index += 1;
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === '--token' || arg.startsWith('--token=')) {
|
|
71
|
+
tokenValue = readOptionValue(argv, index, ['--token']).trim();
|
|
72
|
+
if (!arg.includes('=')) {
|
|
73
|
+
index += 1;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg.startsWith('-')) {
|
|
78
|
+
throw new Error(`Unknown option '${arg}'.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const host = '127.0.0.1';
|
|
83
|
+
const rawPort = Number(portValue ?? process.env.OML_SERVER_PORT ?? '8080');
|
|
84
|
+
const port = Number.isFinite(rawPort) ? Math.max(0, Math.min(65535, Math.trunc(rawPort))) : 8080;
|
|
85
|
+
const workspaceRoot = workspaceValue && workspaceValue.length > 0 ? workspaceValue : process.cwd();
|
|
86
|
+
const token = tokenValue && tokenValue.length > 0 ? tokenValue : undefined;
|
|
87
|
+
return { host, port, workspaceRoot, token };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startIpcLanguageServerIfAvailable(): { connection: import('vscode-languageserver').Connection; runtime: OmlLanguageServerRuntime } | undefined {
|
|
91
|
+
if (typeof process.send !== 'function') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const connection = createConnection();
|
|
95
|
+
const runtime = startOmlLanguageServer(connection, {
|
|
96
|
+
fileSystem: NodeFileSystem,
|
|
97
|
+
registerDiagramHandlers: true,
|
|
98
|
+
suppressTransientDiagnostics: true,
|
|
99
|
+
installNodeProcessHandlers: true,
|
|
100
|
+
});
|
|
101
|
+
return { connection, runtime };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function workspaceHash(workspaceRoot: string): string {
|
|
105
|
+
return createHash('sha256').update(path.resolve(workspaceRoot)).digest('hex');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function lockFilePathForWorkspace(workspaceRoot: string): string {
|
|
109
|
+
return path.join(os.homedir(), '.oml', 'workspaces', workspaceHash(workspaceRoot), 'server.lock');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function writeServerLock(workspaceRoot: string, port: number, owner: ServerLockOwner): Promise<string> {
|
|
113
|
+
const lockFile = lockFilePathForWorkspace(workspaceRoot);
|
|
114
|
+
await fs.mkdir(path.dirname(lockFile), { recursive: true });
|
|
115
|
+
await fs.writeFile(lockFile, JSON.stringify({ port, pid: process.pid, owner }) + '\n', 'utf-8');
|
|
116
|
+
return lockFile;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function removeServerLock(lockFile: string): Promise<void> {
|
|
120
|
+
await fs.rm(lockFile, { force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveListeningPort(server: import('node:http').Server): number {
|
|
124
|
+
const address = server.address();
|
|
125
|
+
if (!address || typeof address === 'string') {
|
|
126
|
+
throw new Error('Unable to resolve listening port.');
|
|
127
|
+
}
|
|
128
|
+
return address.port;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function runServerCli(argv: string[] = process.argv.slice(2)): void {
|
|
132
|
+
let options: RemoteOptions;
|
|
133
|
+
try {
|
|
134
|
+
options = parseRemoteOptions(argv);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
process.stderr.write(`[oml-server] REST startup failed: ${message}\n`);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
process.exit();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const ipcServer = startIpcLanguageServerIfAvailable();
|
|
143
|
+
const owner: ServerLockOwner = ipcServer ? 'extension' : 'daemon';
|
|
144
|
+
void startOmlRestServer({
|
|
145
|
+
host: options.host,
|
|
146
|
+
port: options.port,
|
|
147
|
+
workspaceRoot: options.workspaceRoot,
|
|
148
|
+
watchWorkspace: true,
|
|
149
|
+
authToken: options.token,
|
|
150
|
+
runtime: ipcServer?.runtime,
|
|
151
|
+
}).then(async ({ server, updateToken }) => {
|
|
152
|
+
if (ipcServer) {
|
|
153
|
+
ipcServer.connection.onNotification('$/tokenRefreshed', (params: { accessToken: string }) => {
|
|
154
|
+
if (params?.accessToken) {
|
|
155
|
+
updateToken(params.accessToken);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const listeningPort = resolveListeningPort(server);
|
|
160
|
+
const lockFile = await writeServerLock(options.workspaceRoot, listeningPort, owner);
|
|
161
|
+
let shuttingDown = false;
|
|
162
|
+
const shutdown = (exitCode: number): void => {
|
|
163
|
+
if (shuttingDown) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
shuttingDown = true;
|
|
167
|
+
server.close(() => {
|
|
168
|
+
void removeServerLock(lockFile).finally(() => {
|
|
169
|
+
process.exit(exitCode);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
process.once('SIGINT', () => shutdown(0));
|
|
175
|
+
process.once('SIGTERM', () => shutdown(0));
|
|
176
|
+
server.once('close', () => {
|
|
177
|
+
void removeServerLock(lockFile);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
process.stdout.write(`[oml-server] REST listening on http://${options.host}:${listeningPort}\n`);
|
|
181
|
+
}).catch((error: unknown) => {
|
|
182
|
+
const message = formatStartupError(error, options.host, options.port);
|
|
183
|
+
process.stderr.write(`[oml-server] REST startup failed: ${message}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
process.exit();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
runServerCli();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
export * from './lsp/language-server.js';
|
|
4
|
+
export * from './lsp/diagram-server.js';
|
|
5
|
+
export * from './rest/server.js';
|
|
6
|
+
export * from './lsp/providers/browser-fs-provider.js';
|
|
7
|
+
export * from './lsp/providers/hybrid-fs-provider.js';
|
|
8
|
+
export * from './lsp/protocol/reasoner-protocol.js';
|
|
9
|
+
export * from './lsp/protocol/browser-fs-protocol.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import type { SModelRoot } from 'sprotty-protocol';
|
|
4
|
+
import {
|
|
5
|
+
computeLaidOutSModelForUri,
|
|
6
|
+
resolveDefinition,
|
|
7
|
+
resolveOntologyMemberLabels,
|
|
8
|
+
type OntologyMemberLabelEntry,
|
|
9
|
+
type RangeResponse,
|
|
10
|
+
} from '@oml/language';
|
|
11
|
+
import { RequestType } from 'vscode-languageserver-protocol';
|
|
12
|
+
|
|
13
|
+
type ConnectionLike = {
|
|
14
|
+
onRequest: (type: RequestType<any, any, any>, handler: (params: any) => any | Promise<any>) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DiagramModelRequest = new RequestType<{ modelUri: string }, SModelRoot, void>('oml/diagram/model');
|
|
18
|
+
const DefinitionRequest = new RequestType<{ elementId: string; referencingUri?: string }, RangeResponse | null, void>('oml/definition');
|
|
19
|
+
const OntologyLabelsRequest = new RequestType<{ modelUri: string }, OntologyMemberLabelEntry[], void>('oml/labels');
|
|
20
|
+
|
|
21
|
+
export function registerDiagramRequests(connection: ConnectionLike, shared: any, oml?: any): void {
|
|
22
|
+
connection.onRequest(DiagramModelRequest, async ({ modelUri }) => {
|
|
23
|
+
try {
|
|
24
|
+
return await computeLaidOutSModelForUri(shared, modelUri);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[oml] diagram model error', err);
|
|
27
|
+
return { id: 'root', type: 'graph', children: [] } as unknown as SModelRoot;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
connection.onRequest(DefinitionRequest, async ({ elementId, referencingUri }) => {
|
|
32
|
+
try {
|
|
33
|
+
return await resolveDefinition(shared, elementId, referencingUri);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[oml] definition error', err);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
connection.onRequest(OntologyLabelsRequest, async ({ modelUri }) => {
|
|
41
|
+
try {
|
|
42
|
+
return await resolveOntologyMemberLabels(shared, oml, modelUri);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('[oml] labels error', err);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|