@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,327 @@
|
|
|
1
|
+
import type { GitWorkspaceConfig, FolderEntry, ReadResult, VersionInfo, WriteQueryOptions } from "../types";
|
|
2
|
+
import { BaseGitProviderClient } from "./BaseGitProviderClient";
|
|
3
|
+
import { WorkspaceBackendError } from "./errors";
|
|
4
|
+
import { parseGitRemote } from "./gitRemote";
|
|
5
|
+
|
|
6
|
+
type GitlabProject = {
|
|
7
|
+
default_branch?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type GitlabTreeItem = {
|
|
11
|
+
type: "tree" | "blob";
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type GitlabFileResponse = {
|
|
17
|
+
content?: string;
|
|
18
|
+
encoding?: "base64";
|
|
19
|
+
last_commit_id?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type GitlabCommit = {
|
|
23
|
+
id: string;
|
|
24
|
+
created_at?: string;
|
|
25
|
+
author_name?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function inferApiBase(config: GitWorkspaceConfig, host: string): string {
|
|
30
|
+
const configured = (config as any).apiBaseUrl as string | undefined;
|
|
31
|
+
if (configured?.trim()) return configured.trim().replace(/\/+$/g, "");
|
|
32
|
+
if (host === "gitlab.com") return "https://gitlab.com/api/v4";
|
|
33
|
+
return `https://${host}/api/v4`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GitlabProviderClient extends BaseGitProviderClient {
|
|
37
|
+
public static canHandle(config: GitWorkspaceConfig): boolean {
|
|
38
|
+
const provider = (config as any).provider as string | undefined;
|
|
39
|
+
if (provider && provider !== "auto") return provider === "gitlab";
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
43
|
+
return host === "gitlab.com" || host.includes("gitlab");
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async request<T>(
|
|
50
|
+
apiBase: string,
|
|
51
|
+
config: GitWorkspaceConfig,
|
|
52
|
+
path: string,
|
|
53
|
+
init?: RequestInit,
|
|
54
|
+
): Promise<{ status: number; json?: T }> {
|
|
55
|
+
const headers: Record<string, string> = {
|
|
56
|
+
Accept: "application/json",
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const token = config.auth.token?.trim();
|
|
61
|
+
if (token) headers["PRIVATE-TOKEN"] = token;
|
|
62
|
+
|
|
63
|
+
const res = await fetch(`${apiBase}${path}`, {
|
|
64
|
+
...init,
|
|
65
|
+
// Avoid stale results after create/delete/rename.
|
|
66
|
+
cache: "no-store",
|
|
67
|
+
headers: {
|
|
68
|
+
...headers,
|
|
69
|
+
...(init?.headers as any),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const status = res.status;
|
|
74
|
+
const contentType = res.headers.get("content-type") || "";
|
|
75
|
+
if (contentType.includes("application/json")) {
|
|
76
|
+
const text = await res.text();
|
|
77
|
+
if (text.trim()) {
|
|
78
|
+
try {
|
|
79
|
+
const json = JSON.parse(text) as T;
|
|
80
|
+
return { status, json };
|
|
81
|
+
} catch {
|
|
82
|
+
// Invalid JSON, return status only
|
|
83
|
+
return { status };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { status };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private getProjectId(config: GitWorkspaceConfig): string {
|
|
92
|
+
const { repoPath } = parseGitRemote(config.remoteUrl);
|
|
93
|
+
// GitLab uses the full namespace path, URL-encoded.
|
|
94
|
+
return encodeURIComponent(repoPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async getBranch(config: GitWorkspaceConfig, apiBase: string): Promise<string> {
|
|
98
|
+
const configured = config.branch?.trim();
|
|
99
|
+
if (configured) return configured;
|
|
100
|
+
|
|
101
|
+
const projectId = this.getProjectId(config);
|
|
102
|
+
const { status, json } = await this.request<GitlabProject>(apiBase, config, `/projects/${projectId}`);
|
|
103
|
+
this.ensureOk(status, "Failed to resolve repository default branch.");
|
|
104
|
+
|
|
105
|
+
const branch = json?.default_branch?.trim();
|
|
106
|
+
if (!branch) throw new WorkspaceBackendError("UNKNOWN", "Could not determine default branch.");
|
|
107
|
+
return branch;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async validateAccess(config: GitWorkspaceConfig): Promise<void> {
|
|
111
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
112
|
+
const apiBase = inferApiBase(config, host);
|
|
113
|
+
const projectId = this.getProjectId(config);
|
|
114
|
+
|
|
115
|
+
const { status } = await this.request(apiBase, config, `/projects/${projectId}`);
|
|
116
|
+
this.ensureOk(status, "Could not access GitLab project with the provided token.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<FolderEntry[]> {
|
|
120
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
121
|
+
const apiBase = inferApiBase(config, host);
|
|
122
|
+
|
|
123
|
+
const relPath = folderId?.trim() || "";
|
|
124
|
+
const fullPath = this.joinPath(config.rootPath, relPath);
|
|
125
|
+
|
|
126
|
+
const projectId = this.getProjectId(config);
|
|
127
|
+
const branch = await this.getBranch(config, apiBase);
|
|
128
|
+
|
|
129
|
+
const qsBase = new URLSearchParams();
|
|
130
|
+
qsBase.set("ref", branch);
|
|
131
|
+
qsBase.set("per_page", "100");
|
|
132
|
+
if (fullPath) qsBase.set("path", fullPath);
|
|
133
|
+
|
|
134
|
+
// Paginate to avoid silently missing items.
|
|
135
|
+
const items: GitlabTreeItem[] = [];
|
|
136
|
+
for (let page = 1; page <= 50; page++) {
|
|
137
|
+
const qs = new URLSearchParams(qsBase);
|
|
138
|
+
qs.set("page", String(page));
|
|
139
|
+
|
|
140
|
+
const { status, json } = await this.request<GitlabTreeItem[]>(
|
|
141
|
+
apiBase,
|
|
142
|
+
config,
|
|
143
|
+
`/projects/${projectId}/repository/tree?${qs.toString()}`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (status === 404) return [];
|
|
147
|
+
this.ensureOk(status, "Failed to list folder contents.");
|
|
148
|
+
|
|
149
|
+
const batch = Array.isArray(json) ? json : [];
|
|
150
|
+
items.push(...batch);
|
|
151
|
+
if (batch.length < 100) break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const entries: FolderEntry[] = [];
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
if (item.type === "tree") {
|
|
157
|
+
const id = relPath ? this.joinPath(relPath, item.name) : item.name;
|
|
158
|
+
entries.push({ kind: "folder", id, label: item.name, parentId: relPath || undefined });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (item.type === "blob") {
|
|
163
|
+
if (!/\.sparql$/i.test(item.name)) continue;
|
|
164
|
+
const id = relPath ? this.joinPath(relPath, item.name) : item.name;
|
|
165
|
+
const label = item.name.replace(/\.sparql$/i, "");
|
|
166
|
+
entries.push({ kind: "query", id, label, parentId: relPath || undefined });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
entries.sort((a, b) => {
|
|
171
|
+
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
172
|
+
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async readFileAtRef(
|
|
179
|
+
config: GitWorkspaceConfig,
|
|
180
|
+
apiBase: string,
|
|
181
|
+
queryId: string,
|
|
182
|
+
ref?: string,
|
|
183
|
+
): Promise<{ text: string; lastCommitId?: string }> {
|
|
184
|
+
const projectId = this.getProjectId(config);
|
|
185
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
186
|
+
|
|
187
|
+
const branch = ref || (await this.getBranch(config, apiBase));
|
|
188
|
+
|
|
189
|
+
const { status, json } = await this.request<GitlabFileResponse>(
|
|
190
|
+
apiBase,
|
|
191
|
+
config,
|
|
192
|
+
`/projects/${projectId}/repository/files/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(branch)}`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
this.ensureOk(status, "Failed to read query.");
|
|
196
|
+
|
|
197
|
+
const content = json?.content;
|
|
198
|
+
if (!content) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
199
|
+
|
|
200
|
+
const text = json?.encoding === "base64" ? this.base64DecodeUtf8(content) : content;
|
|
201
|
+
return { text, lastCommitId: json?.last_commit_id };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async readQuery(config: GitWorkspaceConfig, queryId: string): Promise<ReadResult> {
|
|
205
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
206
|
+
const apiBase = inferApiBase(config, host);
|
|
207
|
+
const { text, lastCommitId } = await this.readFileAtRef(config, apiBase, queryId);
|
|
208
|
+
return { queryText: text, versionTag: lastCommitId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async writeQuery(
|
|
212
|
+
config: GitWorkspaceConfig,
|
|
213
|
+
queryId: string,
|
|
214
|
+
queryText: string,
|
|
215
|
+
options?: WriteQueryOptions,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
218
|
+
const apiBase = inferApiBase(config, host);
|
|
219
|
+
|
|
220
|
+
const projectId = this.getProjectId(config);
|
|
221
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
222
|
+
const branch = await this.getBranch(config, apiBase);
|
|
223
|
+
|
|
224
|
+
// Check existence + optimistic concurrency.
|
|
225
|
+
let exists = true;
|
|
226
|
+
let lastCommitId: string | undefined;
|
|
227
|
+
try {
|
|
228
|
+
const res = await this.readFileAtRef(config, apiBase, queryId);
|
|
229
|
+
lastCommitId = res.lastCommitId;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
const err = e as any;
|
|
232
|
+
exists = false;
|
|
233
|
+
if (err?.code && err.code !== "NOT_FOUND") throw e;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options?.expectedVersionTag && lastCommitId && options.expectedVersionTag !== lastCommitId) {
|
|
237
|
+
throw new WorkspaceBackendError(
|
|
238
|
+
"CONFLICT",
|
|
239
|
+
"The file changed remotely since it was last opened. Please reload the managed query and try saving again.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const message = this.getCommitMessage(queryId, options, !exists);
|
|
244
|
+
const body: any = {
|
|
245
|
+
branch,
|
|
246
|
+
content: this.base64EncodeUtf8(queryText),
|
|
247
|
+
encoding: "base64",
|
|
248
|
+
commit_message: message,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const method = exists ? "PUT" : "POST";
|
|
252
|
+
const { status } = await this.request(
|
|
253
|
+
apiBase,
|
|
254
|
+
config,
|
|
255
|
+
`/projects/${projectId}/repository/files/${encodeURIComponent(filePath)}`,
|
|
256
|
+
{
|
|
257
|
+
method,
|
|
258
|
+
body: JSON.stringify(body),
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.ensureOk(status, "Failed to save query to GitLab workspace.");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async listVersions(config: GitWorkspaceConfig, queryId: string): Promise<VersionInfo[]> {
|
|
266
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
267
|
+
const apiBase = inferApiBase(config, host);
|
|
268
|
+
|
|
269
|
+
const projectId = this.getProjectId(config);
|
|
270
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
271
|
+
const branch = await this.getBranch(config, apiBase);
|
|
272
|
+
|
|
273
|
+
const qs = new URLSearchParams();
|
|
274
|
+
qs.set("ref_name", branch);
|
|
275
|
+
qs.set("path", filePath);
|
|
276
|
+
qs.set("per_page", "30");
|
|
277
|
+
|
|
278
|
+
const { status, json } = await this.request<GitlabCommit[]>(
|
|
279
|
+
apiBase,
|
|
280
|
+
config,
|
|
281
|
+
`/projects/${projectId}/repository/commits?${qs.toString()}`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
this.ensureOk(status, "Failed to list query versions.");
|
|
285
|
+
|
|
286
|
+
const commits = Array.isArray(json) ? json : [];
|
|
287
|
+
return commits
|
|
288
|
+
.map((c) => {
|
|
289
|
+
const createdAt = c.created_at || new Date().toISOString();
|
|
290
|
+
return { id: c.id, createdAt, author: c.author_name, message: c.message } satisfies VersionInfo;
|
|
291
|
+
})
|
|
292
|
+
.filter((v) => !!v.id);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async readVersion(config: GitWorkspaceConfig, queryId: string, versionId: string): Promise<ReadResult> {
|
|
296
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
297
|
+
const apiBase = inferApiBase(config, host);
|
|
298
|
+
|
|
299
|
+
const { text, lastCommitId } = await this.readFileAtRef(config, apiBase, queryId, versionId);
|
|
300
|
+
// Use the provider's optimistic-concurrency tag for this file content.
|
|
301
|
+
return { queryText: text, versionTag: lastCommitId };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void> {
|
|
305
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
306
|
+
const apiBase = inferApiBase(config, host);
|
|
307
|
+
|
|
308
|
+
const projectId = this.getProjectId(config);
|
|
309
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
310
|
+
const branch = await this.getBranch(config, apiBase);
|
|
311
|
+
|
|
312
|
+
const body: any = {
|
|
313
|
+
branch,
|
|
314
|
+
commit_message: this.getDeleteMessage(queryId),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const { status } = await this.request(
|
|
318
|
+
apiBase,
|
|
319
|
+
config,
|
|
320
|
+
`/projects/${projectId}/repository/files/${encodeURIComponent(filePath)}`,
|
|
321
|
+
{ method: "DELETE", body: JSON.stringify(body) },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (status === 404) return;
|
|
325
|
+
this.ensureOk(status, "Failed to delete query from GitLab workspace.");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { FolderEntry, ReadResult, VersionInfo, WriteQueryOptions, WorkspaceConfigBase } from "../types";
|
|
2
|
+
import { normalizeQueryText } from "../textHash";
|
|
3
|
+
import type { WorkspaceBackend } from "./WorkspaceBackend";
|
|
4
|
+
import { WorkspaceBackendError } from "./errors";
|
|
5
|
+
|
|
6
|
+
type InMemoryWorkspaceConfig = Pick<WorkspaceConfigBase, "id" | "label"> & {
|
|
7
|
+
type: "memory";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type StoredVersion = {
|
|
11
|
+
id: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
queryText: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function normalizeFolderId(folderId?: string): string {
|
|
17
|
+
if (!folderId) return "";
|
|
18
|
+
return folderId.replace(/^\/+|\/+$/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function splitPath(path: string): string[] {
|
|
22
|
+
return path.split("/").filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function basename(path: string): string {
|
|
26
|
+
const parts = splitPath(path);
|
|
27
|
+
return parts[parts.length - 1] || "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dirname(path: string): string {
|
|
31
|
+
const parts = splitPath(path);
|
|
32
|
+
parts.pop();
|
|
33
|
+
return parts.join("/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nowIso(): string {
|
|
37
|
+
return new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default class InMemoryWorkspaceBackend implements WorkspaceBackend {
|
|
41
|
+
public readonly type = "git" as const;
|
|
42
|
+
|
|
43
|
+
private versionsByQueryId = new Map<string, StoredVersion[]>();
|
|
44
|
+
|
|
45
|
+
constructor(private _config?: InMemoryWorkspaceConfig) {}
|
|
46
|
+
|
|
47
|
+
async validateAccess(): Promise<void> {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async listFolder(folderId?: string): Promise<FolderEntry[]> {
|
|
52
|
+
const folder = normalizeFolderId(folderId);
|
|
53
|
+
|
|
54
|
+
const folders = new Map<string, FolderEntry>();
|
|
55
|
+
const queries: FolderEntry[] = [];
|
|
56
|
+
|
|
57
|
+
for (const queryId of this.versionsByQueryId.keys()) {
|
|
58
|
+
const queryDir = dirname(queryId);
|
|
59
|
+
if (folder === "") {
|
|
60
|
+
const top = splitPath(queryDir)[0];
|
|
61
|
+
if (top) {
|
|
62
|
+
folders.set(top, { kind: "folder", id: top, label: top, parentId: undefined });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
queries.push({
|
|
66
|
+
kind: "query",
|
|
67
|
+
id: queryId,
|
|
68
|
+
label: basename(queryId).replace(/\.sparql$/i, ""),
|
|
69
|
+
parentId: undefined,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (queryDir === folder) {
|
|
75
|
+
queries.push({
|
|
76
|
+
kind: "query",
|
|
77
|
+
id: queryId,
|
|
78
|
+
label: basename(queryId).replace(/\.sparql$/i, ""),
|
|
79
|
+
parentId: folder,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (queryDir.startsWith(folder + "/")) {
|
|
85
|
+
const rest = queryDir.slice(folder.length + 1);
|
|
86
|
+
const child = splitPath(rest)[0];
|
|
87
|
+
if (child) {
|
|
88
|
+
const childId = folder + "/" + child;
|
|
89
|
+
folders.set(childId, { kind: "folder", id: childId, label: child, parentId: folder });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const out = [...folders.values(), ...queries];
|
|
95
|
+
out.sort((a, b) => a.label.localeCompare(b.label));
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async searchByName(query: string): Promise<FolderEntry[]> {
|
|
100
|
+
const q = query.trim().toLowerCase();
|
|
101
|
+
if (!q) return [];
|
|
102
|
+
const hits: FolderEntry[] = [];
|
|
103
|
+
for (const queryId of this.versionsByQueryId.keys()) {
|
|
104
|
+
const label = basename(queryId).replace(/\.sparql$/i, "");
|
|
105
|
+
if (label.toLowerCase().includes(q)) {
|
|
106
|
+
hits.push({ kind: "query", id: queryId, label, parentId: dirname(queryId) || undefined });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
hits.sort((a, b) => a.label.localeCompare(b.label));
|
|
110
|
+
return hits;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async readQuery(queryId: string): Promise<ReadResult> {
|
|
114
|
+
const versions = this.versionsByQueryId.get(queryId);
|
|
115
|
+
if (!versions || versions.length === 0) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
116
|
+
const latest = versions[versions.length - 1];
|
|
117
|
+
return { queryText: latest.queryText, versionTag: latest.id };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async writeQuery(queryId: string, queryText: string, options?: WriteQueryOptions): Promise<void> {
|
|
121
|
+
const versions = this.versionsByQueryId.get(queryId) || [];
|
|
122
|
+
|
|
123
|
+
const latest = versions[versions.length - 1];
|
|
124
|
+
if (options?.expectedVersionTag && latest && options.expectedVersionTag !== latest.id) {
|
|
125
|
+
throw new WorkspaceBackendError("CONFLICT", "Version tag mismatch");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (latest && normalizeQueryText(latest.queryText) === normalizeQueryText(queryText)) {
|
|
129
|
+
this.versionsByQueryId.set(queryId, versions);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nextId = `v${versions.length + 1}`;
|
|
134
|
+
versions.push({ id: nextId, createdAt: nowIso(), queryText });
|
|
135
|
+
this.versionsByQueryId.set(queryId, versions);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async listVersions(queryId: string): Promise<VersionInfo[]> {
|
|
139
|
+
const versions = this.versionsByQueryId.get(queryId);
|
|
140
|
+
if (!versions || versions.length === 0) return [];
|
|
141
|
+
return [...versions]
|
|
142
|
+
.slice()
|
|
143
|
+
.reverse()
|
|
144
|
+
.map((v) => ({ id: v.id, createdAt: v.createdAt }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async readVersion(queryId: string, versionId: string): Promise<ReadResult> {
|
|
148
|
+
const versions = this.versionsByQueryId.get(queryId);
|
|
149
|
+
if (!versions || versions.length === 0) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
150
|
+
const found = versions.find((v) => v.id === versionId);
|
|
151
|
+
if (!found) throw new WorkspaceBackendError("NOT_FOUND", "Version not found");
|
|
152
|
+
return { queryText: found.queryText, versionTag: found.id };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async renameQuery(queryId: string, newLabel: string): Promise<void> {
|
|
156
|
+
const trimmed = newLabel.trim();
|
|
157
|
+
if (!trimmed) throw new WorkspaceBackendError("UNKNOWN", "New name is required");
|
|
158
|
+
|
|
159
|
+
const versions = this.versionsByQueryId.get(queryId);
|
|
160
|
+
if (!versions || versions.length === 0) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
161
|
+
|
|
162
|
+
const newId = dirname(queryId) ? `${dirname(queryId)}/${trimmed}.sparql` : `${trimmed}.sparql`;
|
|
163
|
+
if (newId === queryId) return;
|
|
164
|
+
if (this.versionsByQueryId.has(newId))
|
|
165
|
+
throw new WorkspaceBackendError("CONFLICT", "A query with this name already exists");
|
|
166
|
+
|
|
167
|
+
this.versionsByQueryId.delete(queryId);
|
|
168
|
+
this.versionsByQueryId.set(newId, versions);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async deleteQuery(queryId: string): Promise<void> {
|
|
172
|
+
if (!this.versionsByQueryId.has(queryId)) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
173
|
+
this.versionsByQueryId.delete(queryId);
|
|
174
|
+
}
|
|
175
|
+
}
|