@matdata/yasgui 5.10.0 → 5.11.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 +6 -2
- package/build/ts/src/PersistentConfig.d.ts +10 -0
- package/build/ts/src/PersistentConfig.js +40 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +17 -0
- package/build/ts/src/Tab.js +372 -15
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +1 -0
- package/build/ts/src/TabContextMenu.js +17 -0
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.d.ts +3 -0
- package/build/ts/src/TabElements.js +97 -28
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +2 -0
- package/build/ts/src/TabSettingsModal.js +44 -19
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/index.d.ts +3 -0
- package/build/ts/src/index.js +4 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +64 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +914 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +55 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +451 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +16 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +452 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.d.ts +19 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js +77 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js +269 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +26 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +93 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js +244 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js +252 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js +246 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +21 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +175 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +28 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +687 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +15 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js +2 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/errors.d.ts +7 -0
- package/build/ts/src/queryManagement/backends/errors.js +18 -0
- package/build/ts/src/queryManagement/backends/errors.js.map +1 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.d.ts +8 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js +114 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/gitRemote.d.ts +5 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js +40 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js.map +1 -0
- package/build/ts/src/queryManagement/browserFilter.d.ts +2 -0
- package/build/ts/src/queryManagement/browserFilter.js +7 -0
- package/build/ts/src/queryManagement/browserFilter.js.map +1 -0
- package/build/ts/src/queryManagement/index.d.ts +13 -0
- package/build/ts/src/queryManagement/index.js +14 -0
- package/build/ts/src/queryManagement/index.js.map +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.d.ts +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js +10 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js.map +1 -0
- package/build/ts/src/queryManagement/openManagedQuery.d.ts +15 -0
- package/build/ts/src/queryManagement/openManagedQuery.js +27 -0
- package/build/ts/src/queryManagement/openManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/saveManagedQuery.d.ts +20 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js +109 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/textHash.d.ts +2 -0
- package/build/ts/src/queryManagement/textHash.js +13 -0
- package/build/ts/src/queryManagement/textHash.js.map +1 -0
- package/build/ts/src/queryManagement/types.d.ts +76 -0
- package/build/ts/src/queryManagement/types.js +2 -0
- package/build/ts/src/queryManagement/types.js.map +1 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.d.ts +6 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.js +33 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.js.map +1 -0
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +10 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +398 -172
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/PersistentConfig.ts +61 -0
- package/src/Tab.ts +431 -20
- package/src/TabContextMenu.ts +10 -0
- package/src/TabElements.scss +46 -7
- package/src/TabElements.ts +95 -27
- package/src/TabSettingsModal.scss +102 -5
- package/src/TabSettingsModal.ts +48 -25
- package/src/endpointSelect.scss +2 -3
- package/src/index.scss +4 -0
- package/src/index.ts +7 -0
- package/src/queryManagement/QueryBrowser.scss +418 -0
- package/src/queryManagement/QueryBrowser.ts +1079 -0
- package/src/queryManagement/SaveManagedQueryModal.scss +245 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +554 -0
- package/src/queryManagement/WorkspaceSettingsForm.ts +546 -0
- package/src/queryManagement/backends/BaseGitProviderClient.ts +124 -0
- package/src/queryManagement/backends/BitbucketProviderClient.ts +403 -0
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +96 -0
- package/src/queryManagement/backends/GiteaProviderClient.ts +316 -0
- package/src/queryManagement/backends/GithubProviderClient.ts +319 -0
- package/src/queryManagement/backends/GitlabProviderClient.ts +327 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +175 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +729 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +41 -0
- package/src/queryManagement/backends/errors.ts +28 -0
- package/src/queryManagement/backends/getWorkspaceBackend.ts +132 -0
- package/src/queryManagement/backends/gitRemote.ts +54 -0
- package/src/queryManagement/browserFilter.ts +8 -0
- package/src/queryManagement/index.ts +15 -0
- package/src/queryManagement/normalizeQueryFilename.ts +8 -0
- package/src/queryManagement/openManagedQuery.ts +31 -0
- package/src/queryManagement/saveManagedQuery.ts +135 -0
- package/src/queryManagement/textHash.ts +15 -0
- package/src/queryManagement/types.ts +85 -0
- package/src/queryManagement/validateWorkspaceConfig.ts +40 -0
- package/src/tab.scss +4 -14
- package/src/themes.scss +14 -23
- package/src/version.ts +1 -1
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import type { SparqlWorkspaceConfig, FolderEntry, ReadResult, VersionInfo, WriteQueryOptions } from "../types";
|
|
2
|
+
import { normalizeQueryText } from "../textHash";
|
|
3
|
+
import type { WorkspaceBackend } from "./WorkspaceBackend";
|
|
4
|
+
import { WorkspaceBackendError } from "./errors";
|
|
5
|
+
|
|
6
|
+
type SparqlBindingValue = { type: string; value: string };
|
|
7
|
+
type SparqlBindingsRow = Record<string, SparqlBindingValue | undefined>;
|
|
8
|
+
|
|
9
|
+
type SparqlJsonResults = {
|
|
10
|
+
head?: { vars?: string[] };
|
|
11
|
+
results?: { bindings?: SparqlBindingsRow[] };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function stripTrailingSlash(value: string): string {
|
|
15
|
+
return value.replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function splitPath(path: string): string[] {
|
|
19
|
+
return path.split("/").filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nowIso(): string {
|
|
23
|
+
return new Date().toISOString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sparqlStringLiteral(value: string): string {
|
|
27
|
+
// Keep this conservative; SPARQL supports the same escape sequences as Turtle string literals.
|
|
28
|
+
// Use a normal quoted literal so we don't have to worry about triple-quote edge cases.
|
|
29
|
+
const escaped = value
|
|
30
|
+
.replace(/\\/g, "\\\\")
|
|
31
|
+
.replace(/\r/g, "\\r")
|
|
32
|
+
.replace(/\n/g, "\\n")
|
|
33
|
+
.replace(/\t/g, "\\t")
|
|
34
|
+
.replace(/\"/g, '\\"');
|
|
35
|
+
return `"${escaped}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function iri(iriValue: string): string {
|
|
39
|
+
const trimmed = iriValue.trim();
|
|
40
|
+
if (!trimmed) throw new Error("IRI is required");
|
|
41
|
+
return `<${trimmed.replace(/>/g, "%3E")}>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function uuidV4(): string {
|
|
45
|
+
const cryptoObj = (globalThis as any).crypto as Crypto | undefined;
|
|
46
|
+
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
|
|
47
|
+
// RFC4122 v4 fallback (best-effort)
|
|
48
|
+
const rnds = new Uint8Array(16);
|
|
49
|
+
if (cryptoObj?.getRandomValues) {
|
|
50
|
+
cryptoObj.getRandomValues(rnds);
|
|
51
|
+
} else {
|
|
52
|
+
for (let i = 0; i < rnds.length; i++) rnds[i] = Math.floor(Math.random() * 256);
|
|
53
|
+
}
|
|
54
|
+
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
|
55
|
+
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
|
56
|
+
const hex = Array.from(rnds, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
57
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mintManagedQueryVersionIri(workspaceIri: string): string {
|
|
61
|
+
return `${workspaceIri.trim()}_mq_v_${uuidV4()}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isIriLike(value: string): boolean {
|
|
65
|
+
// Conservative IRI check: scheme ':' ...
|
|
66
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(value.trim());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mintFolderIri(workspaceIri: string, folderPath: string): string {
|
|
70
|
+
const base = stripTrailingSlash(workspaceIri);
|
|
71
|
+
return `${base}/folder/${encodeURIComponent(folderPath)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tryParseFolderPathFromFolderIri(workspaceIri: string, folderIri: string): string | undefined {
|
|
75
|
+
const base = stripTrailingSlash(workspaceIri);
|
|
76
|
+
const prefix = `${base}/folder/`;
|
|
77
|
+
if (!folderIri.startsWith(prefix)) return undefined;
|
|
78
|
+
const encoded = folderIri.slice(prefix.length);
|
|
79
|
+
try {
|
|
80
|
+
return decodeURIComponent(encoded);
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mapHttpError(status: number, message?: string): WorkspaceBackendError {
|
|
87
|
+
if (status === 401) return new WorkspaceBackendError("AUTH_FAILED", message || "Authentication failed");
|
|
88
|
+
if (status === 403) return new WorkspaceBackendError("FORBIDDEN", message || "Forbidden");
|
|
89
|
+
if (status === 404) return new WorkspaceBackendError("NOT_FOUND", message || "Not found");
|
|
90
|
+
if (status === 409) return new WorkspaceBackendError("CONFLICT", message || "Conflict");
|
|
91
|
+
if (status === 429) return new WorkspaceBackendError("RATE_LIMITED", message || "Rate limited");
|
|
92
|
+
if (status >= 500) return new WorkspaceBackendError("NETWORK_ERROR", message || "Server error");
|
|
93
|
+
return new WorkspaceBackendError("UNKNOWN", message || `HTTP ${status}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default class SparqlWorkspaceBackend implements WorkspaceBackend {
|
|
97
|
+
public readonly type = "sparql" as const;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
private config: SparqlWorkspaceConfig,
|
|
101
|
+
private options?: {
|
|
102
|
+
authHeaders?: Record<string, string>;
|
|
103
|
+
getAuthHeaders?: () => Promise<Record<string, string> | undefined>;
|
|
104
|
+
},
|
|
105
|
+
) {}
|
|
106
|
+
|
|
107
|
+
private async resolveAuthHeaders(): Promise<Record<string, string>> {
|
|
108
|
+
try {
|
|
109
|
+
const dynamic = await this.options?.getAuthHeaders?.();
|
|
110
|
+
if (dynamic) return dynamic;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Best-effort: if auth refresh fails, continue without auth so the endpoint can return a clear 401/403.
|
|
113
|
+
console.error("Failed to resolve SPARQL auth headers", e);
|
|
114
|
+
}
|
|
115
|
+
return this.options?.authHeaders || {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async sparqlQuery<T = SparqlJsonResults>(query: string): Promise<T> {
|
|
119
|
+
const body = new URLSearchParams();
|
|
120
|
+
body.set("query", query);
|
|
121
|
+
if (this.config.defaultGraph) body.set("default-graph-uri", this.config.defaultGraph);
|
|
122
|
+
|
|
123
|
+
const authHeaders = await this.resolveAuthHeaders();
|
|
124
|
+
|
|
125
|
+
let res: Response;
|
|
126
|
+
try {
|
|
127
|
+
res = await fetch(this.config.endpoint, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
Accept: "application/sparql-results+json",
|
|
131
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
132
|
+
...authHeaders,
|
|
133
|
+
},
|
|
134
|
+
body: body.toString(),
|
|
135
|
+
});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
throw new WorkspaceBackendError("NETWORK_ERROR", e instanceof Error ? e.message : String(e));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const text = await res.text().catch(() => "");
|
|
142
|
+
throw mapHttpError(res.status, text || `SPARQL endpoint responded with ${res.status}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
return (await res.json()) as T;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
throw new WorkspaceBackendError("UNKNOWN", e instanceof Error ? e.message : "Invalid SPARQL JSON response");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async sparqlUpdate(update: string): Promise<void> {
|
|
153
|
+
const body = new URLSearchParams();
|
|
154
|
+
body.set("update", update);
|
|
155
|
+
if (this.config.defaultGraph) body.set("default-graph-uri", this.config.defaultGraph);
|
|
156
|
+
|
|
157
|
+
const authHeaders = await this.resolveAuthHeaders();
|
|
158
|
+
|
|
159
|
+
let res: Response;
|
|
160
|
+
try {
|
|
161
|
+
res = await fetch(this.config.endpoint, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
Accept: "application/sparql-results+json, */*",
|
|
165
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
166
|
+
...authHeaders,
|
|
167
|
+
},
|
|
168
|
+
body: body.toString(),
|
|
169
|
+
});
|
|
170
|
+
} catch (e) {
|
|
171
|
+
throw new WorkspaceBackendError("NETWORK_ERROR", e instanceof Error ? e.message : String(e));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const text = await res.text().catch(() => "");
|
|
176
|
+
throw mapHttpError(res.status, text || `SPARQL endpoint responded with ${res.status}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private getBindings(results: SparqlJsonResults): SparqlBindingsRow[] {
|
|
181
|
+
return results?.results?.bindings || [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private folderIriFromId(folderId?: string): string {
|
|
185
|
+
if (!folderId) return this.config.workspaceIri;
|
|
186
|
+
return mintFolderIri(this.config.workspaceIri, folderId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private resolveManagedQueryIri(queryIdOrIri: string): string {
|
|
190
|
+
const trimmed = queryIdOrIri.trim();
|
|
191
|
+
if (!trimmed) throw new WorkspaceBackendError("UNKNOWN", "Query ID is required");
|
|
192
|
+
|
|
193
|
+
// SPARQL workspace query IDs are immutable IRIs.
|
|
194
|
+
if (isIriLike(trimmed)) return trimmed;
|
|
195
|
+
throw new WorkspaceBackendError("UNKNOWN", "SPARQL managed query IDs must be IRIs");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async validateAccess(): Promise<void> {
|
|
199
|
+
// Minimal check: endpoint is reachable and accepts SELECT.
|
|
200
|
+
await this.sparqlQuery(`SELECT (1 AS ?ok) WHERE {} LIMIT 1`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async listFolder(folderId?: string): Promise<FolderEntry[]> {
|
|
204
|
+
const workspaceIri = this.config.workspaceIri;
|
|
205
|
+
const containerIri = this.folderIriFromId(folderId);
|
|
206
|
+
|
|
207
|
+
const folderFilter = folderId ? `?folder skos:broader ${iri(containerIri)} .` : `FILTER(!BOUND(?parent))`;
|
|
208
|
+
|
|
209
|
+
const foldersQuery = `
|
|
210
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
211
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
212
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
213
|
+
|
|
214
|
+
SELECT ?folder ?label ?parent WHERE {
|
|
215
|
+
?folder a yasgui:WorkspaceFolder ;
|
|
216
|
+
skos:inScheme ${iri(workspaceIri)} ;
|
|
217
|
+
rdfs:label ?label .
|
|
218
|
+
OPTIONAL { ?folder skos:broader ?parent }
|
|
219
|
+
${folderFilter}
|
|
220
|
+
}
|
|
221
|
+
ORDER BY LCASE(STR(?label))`;
|
|
222
|
+
|
|
223
|
+
const queriesQuery = `
|
|
224
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
225
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
226
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
227
|
+
|
|
228
|
+
SELECT ?mq ?label WHERE {
|
|
229
|
+
?mq a yasgui:ManagedQuery ;
|
|
230
|
+
rdfs:label ?label ;
|
|
231
|
+
dcterms:isPartOf ${iri(containerIri)} .
|
|
232
|
+
}
|
|
233
|
+
ORDER BY LCASE(STR(?label))`;
|
|
234
|
+
|
|
235
|
+
const [foldersRes, queriesRes] = await Promise.all([
|
|
236
|
+
this.sparqlQuery<SparqlJsonResults>(foldersQuery),
|
|
237
|
+
this.sparqlQuery<SparqlJsonResults>(queriesQuery),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const folders: FolderEntry[] = [];
|
|
241
|
+
for (const row of this.getBindings(foldersRes)) {
|
|
242
|
+
const folder = row.folder?.value;
|
|
243
|
+
const label = row.label?.value;
|
|
244
|
+
if (!folder || !label) continue;
|
|
245
|
+
|
|
246
|
+
const folderPath = tryParseFolderPathFromFolderIri(workspaceIri, folder) || label;
|
|
247
|
+
const parentIri = row.parent?.value;
|
|
248
|
+
const parentPath = parentIri ? tryParseFolderPathFromFolderIri(workspaceIri, parentIri) : undefined;
|
|
249
|
+
|
|
250
|
+
folders.push({
|
|
251
|
+
kind: "folder",
|
|
252
|
+
id: folderPath,
|
|
253
|
+
label,
|
|
254
|
+
parentId: parentPath,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const queries: FolderEntry[] = [];
|
|
259
|
+
for (const row of this.getBindings(queriesRes)) {
|
|
260
|
+
const mqIri = row.mq?.value;
|
|
261
|
+
const label = row.label?.value;
|
|
262
|
+
if (!mqIri || !label) continue;
|
|
263
|
+
queries.push({
|
|
264
|
+
kind: "query",
|
|
265
|
+
id: mqIri,
|
|
266
|
+
label,
|
|
267
|
+
parentId: folderId || undefined,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const out = [...folders, ...queries];
|
|
272
|
+
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async searchByName(query: string): Promise<FolderEntry[]> {
|
|
277
|
+
const q = query.trim();
|
|
278
|
+
if (!q) return [];
|
|
279
|
+
|
|
280
|
+
const workspaceIri = this.config.workspaceIri;
|
|
281
|
+
const queryText = `
|
|
282
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
283
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
284
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
285
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
286
|
+
|
|
287
|
+
SELECT ?mq ?label ?container WHERE {
|
|
288
|
+
?mq a yasgui:ManagedQuery ;
|
|
289
|
+
rdfs:label ?label ;
|
|
290
|
+
dcterms:isPartOf ?container .
|
|
291
|
+
|
|
292
|
+
{
|
|
293
|
+
FILTER(?container = ${iri(workspaceIri)})
|
|
294
|
+
} UNION {
|
|
295
|
+
?container a yasgui:WorkspaceFolder ; skos:inScheme ${iri(workspaceIri)} .
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
FILTER(CONTAINS(LCASE(STR(?label)), LCASE(${sparqlStringLiteral(q)})))
|
|
299
|
+
}
|
|
300
|
+
ORDER BY LCASE(STR(?label))`;
|
|
301
|
+
|
|
302
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(queryText);
|
|
303
|
+
const hits: FolderEntry[] = [];
|
|
304
|
+
|
|
305
|
+
for (const row of this.getBindings(res)) {
|
|
306
|
+
const mqIri = row.mq?.value;
|
|
307
|
+
const label = row.label?.value;
|
|
308
|
+
const container = row.container?.value;
|
|
309
|
+
if (!mqIri || !label || !container) continue;
|
|
310
|
+
const parentId =
|
|
311
|
+
container === workspaceIri ? undefined : tryParseFolderPathFromFolderIri(workspaceIri, container) || undefined;
|
|
312
|
+
hits.push({ kind: "query", id: mqIri, label, parentId });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
hits.sort((a, b) => a.label.localeCompare(b.label));
|
|
316
|
+
return hits;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async readQuery(queryId: string): Promise<ReadResult> {
|
|
320
|
+
const mqIri = this.resolveManagedQueryIri(queryId);
|
|
321
|
+
|
|
322
|
+
const query = `
|
|
323
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
324
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
325
|
+
PREFIX prov: <http://www.w3.org/ns/prov#>
|
|
326
|
+
PREFIX spin: <http://spinrdf.org/spin#>
|
|
327
|
+
PREFIX sd: <http://www.w3.org/ns/sparql-service-description#>
|
|
328
|
+
|
|
329
|
+
SELECT ?version ?created ?text ?endpoint ?description WHERE {
|
|
330
|
+
?version a yasgui:ManagedQueryVersion ;
|
|
331
|
+
dcterms:isVersionOf ${iri(mqIri)} ;
|
|
332
|
+
dcterms:created ?created ;
|
|
333
|
+
spin:text ?text .
|
|
334
|
+
|
|
335
|
+
OPTIONAL { ?version dcterms:description ?description }
|
|
336
|
+
|
|
337
|
+
OPTIONAL {
|
|
338
|
+
?version prov:used ?svc .
|
|
339
|
+
?svc a sd:Service ;
|
|
340
|
+
sd:endpoint ?endpoint .
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
}
|
|
344
|
+
ORDER BY DESC(?created)
|
|
345
|
+
LIMIT 1`;
|
|
346
|
+
|
|
347
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(query);
|
|
348
|
+
const row = this.getBindings(res)[0];
|
|
349
|
+
if (!row) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
350
|
+
const text = row.text?.value;
|
|
351
|
+
const version = row.version?.value;
|
|
352
|
+
const endpoint = row.endpoint?.value;
|
|
353
|
+
const description = row.description?.value;
|
|
354
|
+
if (!text || !version) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
queryText: text,
|
|
358
|
+
versionTag: version,
|
|
359
|
+
associatedEndpoint: endpoint,
|
|
360
|
+
description,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async renameQuery(queryId: string, newLabel: string): Promise<void> {
|
|
365
|
+
const label = newLabel.trim();
|
|
366
|
+
if (!label) throw new WorkspaceBackendError("UNKNOWN", "New name is required");
|
|
367
|
+
const mqIriValue = this.resolveManagedQueryIri(queryId);
|
|
368
|
+
|
|
369
|
+
// Determine the container to enforce uniqueness within the same folder/workspace.
|
|
370
|
+
const containerIriValue = await (async () => {
|
|
371
|
+
const q = `
|
|
372
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
373
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
374
|
+
|
|
375
|
+
SELECT ?container WHERE {
|
|
376
|
+
${iri(mqIriValue)} a yasgui:ManagedQuery ;
|
|
377
|
+
dcterms:isPartOf ?container .
|
|
378
|
+
}
|
|
379
|
+
LIMIT 1`;
|
|
380
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(q);
|
|
381
|
+
const row = this.getBindings(res)[0];
|
|
382
|
+
const container = row?.container?.value;
|
|
383
|
+
if (!container) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
384
|
+
return container;
|
|
385
|
+
})();
|
|
386
|
+
|
|
387
|
+
// Prevent duplicate query names in the same folder (case-insensitive).
|
|
388
|
+
{
|
|
389
|
+
const dupCheck = `
|
|
390
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
391
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
392
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
393
|
+
|
|
394
|
+
SELECT ?mq WHERE {
|
|
395
|
+
?mq a yasgui:ManagedQuery ;
|
|
396
|
+
dcterms:isPartOf ${iri(containerIriValue)} ;
|
|
397
|
+
rdfs:label ?lbl .
|
|
398
|
+
FILTER(LCASE(STR(?lbl)) = LCASE(${sparqlStringLiteral(label)}))
|
|
399
|
+
FILTER(?mq != ${iri(mqIriValue)})
|
|
400
|
+
}
|
|
401
|
+
LIMIT 1`;
|
|
402
|
+
|
|
403
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(dupCheck);
|
|
404
|
+
const row = this.getBindings(res)[0];
|
|
405
|
+
if (row?.mq?.value) {
|
|
406
|
+
throw new WorkspaceBackendError(
|
|
407
|
+
"CONFLICT",
|
|
408
|
+
`A query named '${label}' already exists in this folder. Please choose a different name.`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const mqIri = mqIriValue;
|
|
414
|
+
const update = `
|
|
415
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
416
|
+
|
|
417
|
+
DELETE { ${iri(mqIri)} rdfs:label ?oldLabel . }
|
|
418
|
+
INSERT { ${iri(mqIri)} rdfs:label ${sparqlStringLiteral(label)} . }
|
|
419
|
+
WHERE { OPTIONAL { ${iri(mqIri)} rdfs:label ?oldLabel . } }
|
|
420
|
+
`;
|
|
421
|
+
|
|
422
|
+
await this.sparqlUpdate(update);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async deleteQuery(queryId: string): Promise<void> {
|
|
426
|
+
const mqIri = this.resolveManagedQueryIri(queryId);
|
|
427
|
+
const update = `
|
|
428
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
429
|
+
|
|
430
|
+
DELETE {
|
|
431
|
+
${iri(mqIri)} ?p ?o .
|
|
432
|
+
?s ?pp ${iri(mqIri)} .
|
|
433
|
+
?v ?vp ?vo .
|
|
434
|
+
?sv ?svP ?v .
|
|
435
|
+
}
|
|
436
|
+
WHERE {
|
|
437
|
+
OPTIONAL { ${iri(mqIri)} ?p ?o . }
|
|
438
|
+
OPTIONAL { ?s ?pp ${iri(mqIri)} . }
|
|
439
|
+
OPTIONAL {
|
|
440
|
+
?v dcterms:isVersionOf ${iri(mqIri)} .
|
|
441
|
+
OPTIONAL { ?v ?vp ?vo . }
|
|
442
|
+
OPTIONAL { ?sv ?svP ?v . }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
|
|
447
|
+
await this.sparqlUpdate(update);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async renameFolder(folderId: string, newLabel: string): Promise<void> {
|
|
451
|
+
const label = newLabel.trim();
|
|
452
|
+
if (!label) throw new WorkspaceBackendError("UNKNOWN", "New name is required");
|
|
453
|
+
|
|
454
|
+
const folderIriValue = mintFolderIri(this.config.workspaceIri, folderId);
|
|
455
|
+
const update = `
|
|
456
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
457
|
+
|
|
458
|
+
DELETE { ${iri(folderIriValue)} rdfs:label ?oldLabel . }
|
|
459
|
+
INSERT { ${iri(folderIriValue)} rdfs:label ${sparqlStringLiteral(label)} . }
|
|
460
|
+
WHERE { OPTIONAL { ${iri(folderIriValue)} rdfs:label ?oldLabel . } }
|
|
461
|
+
`;
|
|
462
|
+
|
|
463
|
+
await this.sparqlUpdate(update);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async deleteFolder(folderId: string): Promise<void> {
|
|
467
|
+
const workspaceIri = this.config.workspaceIri;
|
|
468
|
+
const rootFolderIri = mintFolderIri(workspaceIri, folderId);
|
|
469
|
+
const update = `
|
|
470
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
471
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
472
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
473
|
+
|
|
474
|
+
DELETE {
|
|
475
|
+
?folder ?fp ?fo .
|
|
476
|
+
?sf ?sfp ?folder .
|
|
477
|
+
|
|
478
|
+
?mq ?mp ?mo .
|
|
479
|
+
?sm ?smp ?mq .
|
|
480
|
+
|
|
481
|
+
?v ?vp ?vo .
|
|
482
|
+
?sv ?svP ?v .
|
|
483
|
+
}
|
|
484
|
+
WHERE {
|
|
485
|
+
{
|
|
486
|
+
SELECT DISTINCT ?folder WHERE {
|
|
487
|
+
?folder a yasgui:WorkspaceFolder ;
|
|
488
|
+
skos:inScheme ${iri(workspaceIri)} .
|
|
489
|
+
?folder (skos:broader)* ${iri(rootFolderIri)} .
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
OPTIONAL { ?folder ?fp ?fo . }
|
|
494
|
+
OPTIONAL { ?sf ?sfp ?folder . }
|
|
495
|
+
|
|
496
|
+
OPTIONAL {
|
|
497
|
+
?mq a yasgui:ManagedQuery ;
|
|
498
|
+
dcterms:isPartOf ?folder .
|
|
499
|
+
OPTIONAL { ?mq ?mp ?mo . }
|
|
500
|
+
OPTIONAL { ?sm ?smp ?mq . }
|
|
501
|
+
|
|
502
|
+
OPTIONAL {
|
|
503
|
+
?v dcterms:isVersionOf ?mq .
|
|
504
|
+
OPTIONAL { ?v ?vp ?vo . }
|
|
505
|
+
OPTIONAL { ?sv ?svP ?v . }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
`;
|
|
510
|
+
|
|
511
|
+
await this.sparqlUpdate(update);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async writeQuery(queryId: string, queryText: string, options?: WriteQueryOptions): Promise<void> {
|
|
515
|
+
const workspaceIri = this.config.workspaceIri;
|
|
516
|
+
const mqIriValue = this.resolveManagedQueryIri(queryId);
|
|
517
|
+
|
|
518
|
+
// For new queries (Save-as-managed), UI provides label/folder.
|
|
519
|
+
// For saving existing managed queries, label/folder might be omitted; derive/fetch them.
|
|
520
|
+
let folderPath = (options?.folderId || "").trim();
|
|
521
|
+
let label = (options?.label || "").trim();
|
|
522
|
+
|
|
523
|
+
if (!label) {
|
|
524
|
+
// QueryId is an IRI; fetch current label + container from the store.
|
|
525
|
+
const q = `
|
|
526
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
527
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
528
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
529
|
+
|
|
530
|
+
SELECT ?label ?container WHERE {
|
|
531
|
+
${iri(mqIriValue)} a yasgui:ManagedQuery ;
|
|
532
|
+
rdfs:label ?label ;
|
|
533
|
+
dcterms:isPartOf ?container .
|
|
534
|
+
}
|
|
535
|
+
LIMIT 1`;
|
|
536
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(q);
|
|
537
|
+
const row = this.getBindings(res)[0];
|
|
538
|
+
const fetchedLabel = row?.label?.value;
|
|
539
|
+
const container = row?.container?.value;
|
|
540
|
+
if (!fetchedLabel || !container) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
541
|
+
label = fetchedLabel;
|
|
542
|
+
if (!folderPath && container !== workspaceIri) {
|
|
543
|
+
folderPath = tryParseFolderPathFromFolderIri(workspaceIri, container) || "";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!label.trim()) throw new WorkspaceBackendError("UNKNOWN", "Query name is required");
|
|
548
|
+
|
|
549
|
+
const containerIriValue = folderPath ? mintFolderIri(workspaceIri, folderPath) : workspaceIri;
|
|
550
|
+
|
|
551
|
+
// Prevent duplicate query names in the same folder. This can happen if an existing query
|
|
552
|
+
// was renamed to this label (so queryId differs), and a new query is then created.
|
|
553
|
+
// Compare case-insensitively to avoid near-identical duplicates.
|
|
554
|
+
{
|
|
555
|
+
const dupCheck = `
|
|
556
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
557
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
558
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
559
|
+
|
|
560
|
+
SELECT ?mq WHERE {
|
|
561
|
+
?mq a yasgui:ManagedQuery ;
|
|
562
|
+
dcterms:isPartOf ${iri(containerIriValue)} ;
|
|
563
|
+
rdfs:label ?lbl .
|
|
564
|
+
FILTER(LCASE(STR(?lbl)) = LCASE(${sparqlStringLiteral(label)}))
|
|
565
|
+
FILTER(?mq != ${iri(mqIriValue)})
|
|
566
|
+
}
|
|
567
|
+
LIMIT 1`;
|
|
568
|
+
|
|
569
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(dupCheck);
|
|
570
|
+
const row = this.getBindings(res)[0];
|
|
571
|
+
if (row?.mq?.value) {
|
|
572
|
+
throw new WorkspaceBackendError(
|
|
573
|
+
"CONFLICT",
|
|
574
|
+
`A query named '${label}' already exists in this folder. Please choose a different name.`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Conflict check (best-effort; not atomic across concurrent writers).
|
|
580
|
+
if (options?.expectedVersionTag) {
|
|
581
|
+
try {
|
|
582
|
+
const current = await this.readQuery(queryId);
|
|
583
|
+
if (current.versionTag && current.versionTag !== options.expectedVersionTag) {
|
|
584
|
+
throw new WorkspaceBackendError("CONFLICT", "Version tag mismatch");
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
const err = e as any;
|
|
588
|
+
if (err?.code && err.code !== "NOT_FOUND") throw e;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Versioning rule: don't create a new version if normalized text is unchanged.
|
|
593
|
+
try {
|
|
594
|
+
const current = await this.readQuery(queryId);
|
|
595
|
+
if (normalizeQueryText(current.queryText) === normalizeQueryText(queryText)) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {
|
|
599
|
+
const err = e as any;
|
|
600
|
+
if (err?.code && err.code !== "NOT_FOUND") throw e;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const versionIriValue = mintManagedQueryVersionIri(workspaceIri);
|
|
604
|
+
const createdAt = nowIso();
|
|
605
|
+
const description = options?.message?.trim();
|
|
606
|
+
const associatedEndpoint = options?.associatedEndpoint?.trim();
|
|
607
|
+
|
|
608
|
+
const folderTriples: string[] = [];
|
|
609
|
+
if (folderPath) {
|
|
610
|
+
// Ensure each folder in the path exists and is linked to the workspace scheme.
|
|
611
|
+
const parts = splitPath(folderPath);
|
|
612
|
+
for (let i = 0; i < parts.length; i++) {
|
|
613
|
+
const subPath = parts.slice(0, i + 1).join("/");
|
|
614
|
+
const folderIriValue = mintFolderIri(workspaceIri, subPath);
|
|
615
|
+
const folderLabel = parts[i];
|
|
616
|
+
folderTriples.push(`${iri(folderIriValue)} a <https://matdata.eu/ns/yasgui#WorkspaceFolder> ;`);
|
|
617
|
+
folderTriples.push(` <http://www.w3.org/2004/02/skos/core#inScheme> ${iri(workspaceIri)} ;`);
|
|
618
|
+
folderTriples.push(` <http://www.w3.org/2000/01/rdf-schema#label> ${sparqlStringLiteral(folderLabel)} .`);
|
|
619
|
+
|
|
620
|
+
if (i > 0) {
|
|
621
|
+
const parentPath = parts.slice(0, i).join("/");
|
|
622
|
+
const parentIri = mintFolderIri(workspaceIri, parentPath);
|
|
623
|
+
folderTriples.push(
|
|
624
|
+
`${iri(folderIriValue)} <http://www.w3.org/2004/02/skos/core#broader> ${iri(parentIri)} .`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const update = `
|
|
631
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
632
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
633
|
+
PREFIX prov: <http://www.w3.org/ns/prov#>
|
|
634
|
+
PREFIX sd: <http://www.w3.org/ns/sparql-service-description#>
|
|
635
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
636
|
+
PREFIX spin: <http://spinrdf.org/spin#>
|
|
637
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
638
|
+
|
|
639
|
+
INSERT DATA {
|
|
640
|
+
${iri(workspaceIri)} a yasgui:Workspace .
|
|
641
|
+
|
|
642
|
+
${folderTriples.join("\n ")}
|
|
643
|
+
|
|
644
|
+
${iri(mqIriValue)} a yasgui:ManagedQuery ;
|
|
645
|
+
rdfs:label ${sparqlStringLiteral(label)} ;
|
|
646
|
+
dcterms:isPartOf ${iri(containerIriValue)} .
|
|
647
|
+
|
|
648
|
+
${iri(versionIriValue)} a yasgui:ManagedQueryVersion ;
|
|
649
|
+
dcterms:isVersionOf ${iri(mqIriValue)} ;
|
|
650
|
+
dcterms:created ${sparqlStringLiteral(createdAt)}^^<http://www.w3.org/2001/XMLSchema#dateTime> ;
|
|
651
|
+
spin:text ${sparqlStringLiteral(queryText)}${description ? `;\n dcterms:description ${sparqlStringLiteral(description)}` : ""}${associatedEndpoint ? `;\n prov:used _:svc` : ""} .
|
|
652
|
+
|
|
653
|
+
${associatedEndpoint ? `_:svc a sd:Service ;\n sd:endpoint ${iri(associatedEndpoint)} .` : ""}
|
|
654
|
+
}`;
|
|
655
|
+
|
|
656
|
+
await this.sparqlUpdate(update);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async listVersions(queryId: string): Promise<VersionInfo[]> {
|
|
660
|
+
const mqIri = this.resolveManagedQueryIri(queryId);
|
|
661
|
+
|
|
662
|
+
const query = `
|
|
663
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
664
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
665
|
+
|
|
666
|
+
SELECT ?version ?created ?creator ?message WHERE {
|
|
667
|
+
?version a yasgui:ManagedQueryVersion ;
|
|
668
|
+
dcterms:isVersionOf ${iri(mqIri)} ;
|
|
669
|
+
dcterms:created ?created .
|
|
670
|
+
OPTIONAL { ?version dcterms:creator ?creator }
|
|
671
|
+
OPTIONAL { ?version dcterms:description ?message }
|
|
672
|
+
}
|
|
673
|
+
ORDER BY DESC(?created)`;
|
|
674
|
+
|
|
675
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(query);
|
|
676
|
+
const versions: VersionInfo[] = [];
|
|
677
|
+
|
|
678
|
+
for (const row of this.getBindings(res)) {
|
|
679
|
+
const version = row.version?.value;
|
|
680
|
+
const createdAt = row.created?.value;
|
|
681
|
+
if (!version || !createdAt) continue;
|
|
682
|
+
versions.push({
|
|
683
|
+
id: version,
|
|
684
|
+
createdAt,
|
|
685
|
+
author: row.creator?.value,
|
|
686
|
+
message: row.message?.value,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return versions;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async readVersion(queryId: string, versionId: string): Promise<ReadResult> {
|
|
694
|
+
const mqIri = this.resolveManagedQueryIri(queryId);
|
|
695
|
+
|
|
696
|
+
const query = `
|
|
697
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
698
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
699
|
+
PREFIX prov: <http://www.w3.org/ns/prov#>
|
|
700
|
+
PREFIX spin: <http://spinrdf.org/spin#>
|
|
701
|
+
PREFIX sd: <http://www.w3.org/ns/sparql-service-description#>
|
|
702
|
+
|
|
703
|
+
SELECT ?text ?endpoint ?description WHERE {
|
|
704
|
+
${iri(versionId)} a yasgui:ManagedQueryVersion ;
|
|
705
|
+
dcterms:isVersionOf ${iri(mqIri)} ;
|
|
706
|
+
spin:text ?text .
|
|
707
|
+
|
|
708
|
+
OPTIONAL { ${iri(versionId)} dcterms:description ?description }
|
|
709
|
+
OPTIONAL {
|
|
710
|
+
${iri(versionId)} prov:used ?svc .
|
|
711
|
+
?svc a sd:Service ;
|
|
712
|
+
sd:endpoint ?endpoint .
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
LIMIT 1`;
|
|
716
|
+
|
|
717
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(query);
|
|
718
|
+
const row = this.getBindings(res)[0];
|
|
719
|
+
if (!row) throw new WorkspaceBackendError("NOT_FOUND", "Version not found");
|
|
720
|
+
const text = row.text?.value;
|
|
721
|
+
if (!text) throw new WorkspaceBackendError("NOT_FOUND", "Version not found");
|
|
722
|
+
return {
|
|
723
|
+
queryText: text,
|
|
724
|
+
versionTag: versionId,
|
|
725
|
+
associatedEndpoint: row.endpoint?.value,
|
|
726
|
+
description: row.description?.value,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|