@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.
Files changed (133) hide show
  1. package/README.md +6 -2
  2. package/build/ts/src/PersistentConfig.d.ts +10 -0
  3. package/build/ts/src/PersistentConfig.js +40 -0
  4. package/build/ts/src/PersistentConfig.js.map +1 -1
  5. package/build/ts/src/Tab.d.ts +17 -0
  6. package/build/ts/src/Tab.js +372 -15
  7. package/build/ts/src/Tab.js.map +1 -1
  8. package/build/ts/src/TabContextMenu.d.ts +1 -0
  9. package/build/ts/src/TabContextMenu.js +17 -0
  10. package/build/ts/src/TabContextMenu.js.map +1 -1
  11. package/build/ts/src/TabElements.d.ts +3 -0
  12. package/build/ts/src/TabElements.js +97 -28
  13. package/build/ts/src/TabElements.js.map +1 -1
  14. package/build/ts/src/TabSettingsModal.d.ts +2 -0
  15. package/build/ts/src/TabSettingsModal.js +44 -19
  16. package/build/ts/src/TabSettingsModal.js.map +1 -1
  17. package/build/ts/src/index.d.ts +3 -0
  18. package/build/ts/src/index.js +4 -0
  19. package/build/ts/src/index.js.map +1 -1
  20. package/build/ts/src/queryManagement/QueryBrowser.d.ts +64 -0
  21. package/build/ts/src/queryManagement/QueryBrowser.js +914 -0
  22. package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -0
  23. package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +55 -0
  24. package/build/ts/src/queryManagement/SaveManagedQueryModal.js +451 -0
  25. package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -0
  26. package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +16 -0
  27. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +452 -0
  28. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -0
  29. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.d.ts +19 -0
  30. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js +77 -0
  31. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js.map +1 -0
  32. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.d.ts +16 -0
  33. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js +269 -0
  34. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js.map +1 -0
  35. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +26 -0
  36. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +93 -0
  37. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -0
  38. package/build/ts/src/queryManagement/backends/GiteaProviderClient.d.ts +14 -0
  39. package/build/ts/src/queryManagement/backends/GiteaProviderClient.js +244 -0
  40. package/build/ts/src/queryManagement/backends/GiteaProviderClient.js.map +1 -0
  41. package/build/ts/src/queryManagement/backends/GithubProviderClient.d.ts +14 -0
  42. package/build/ts/src/queryManagement/backends/GithubProviderClient.js +252 -0
  43. package/build/ts/src/queryManagement/backends/GithubProviderClient.js.map +1 -0
  44. package/build/ts/src/queryManagement/backends/GitlabProviderClient.d.ts +16 -0
  45. package/build/ts/src/queryManagement/backends/GitlabProviderClient.js +246 -0
  46. package/build/ts/src/queryManagement/backends/GitlabProviderClient.js.map +1 -0
  47. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +21 -0
  48. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +175 -0
  49. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -0
  50. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +28 -0
  51. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +687 -0
  52. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -0
  53. package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +15 -0
  54. package/build/ts/src/queryManagement/backends/WorkspaceBackend.js +2 -0
  55. package/build/ts/src/queryManagement/backends/WorkspaceBackend.js.map +1 -0
  56. package/build/ts/src/queryManagement/backends/errors.d.ts +7 -0
  57. package/build/ts/src/queryManagement/backends/errors.js +18 -0
  58. package/build/ts/src/queryManagement/backends/errors.js.map +1 -0
  59. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.d.ts +8 -0
  60. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js +114 -0
  61. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js.map +1 -0
  62. package/build/ts/src/queryManagement/backends/gitRemote.d.ts +5 -0
  63. package/build/ts/src/queryManagement/backends/gitRemote.js +40 -0
  64. package/build/ts/src/queryManagement/backends/gitRemote.js.map +1 -0
  65. package/build/ts/src/queryManagement/browserFilter.d.ts +2 -0
  66. package/build/ts/src/queryManagement/browserFilter.js +7 -0
  67. package/build/ts/src/queryManagement/browserFilter.js.map +1 -0
  68. package/build/ts/src/queryManagement/index.d.ts +13 -0
  69. package/build/ts/src/queryManagement/index.js +14 -0
  70. package/build/ts/src/queryManagement/index.js.map +1 -0
  71. package/build/ts/src/queryManagement/normalizeQueryFilename.d.ts +1 -0
  72. package/build/ts/src/queryManagement/normalizeQueryFilename.js +10 -0
  73. package/build/ts/src/queryManagement/normalizeQueryFilename.js.map +1 -0
  74. package/build/ts/src/queryManagement/openManagedQuery.d.ts +15 -0
  75. package/build/ts/src/queryManagement/openManagedQuery.js +27 -0
  76. package/build/ts/src/queryManagement/openManagedQuery.js.map +1 -0
  77. package/build/ts/src/queryManagement/saveManagedQuery.d.ts +20 -0
  78. package/build/ts/src/queryManagement/saveManagedQuery.js +109 -0
  79. package/build/ts/src/queryManagement/saveManagedQuery.js.map +1 -0
  80. package/build/ts/src/queryManagement/textHash.d.ts +2 -0
  81. package/build/ts/src/queryManagement/textHash.js +13 -0
  82. package/build/ts/src/queryManagement/textHash.js.map +1 -0
  83. package/build/ts/src/queryManagement/types.d.ts +76 -0
  84. package/build/ts/src/queryManagement/types.js +2 -0
  85. package/build/ts/src/queryManagement/types.js.map +1 -0
  86. package/build/ts/src/queryManagement/validateWorkspaceConfig.d.ts +6 -0
  87. package/build/ts/src/queryManagement/validateWorkspaceConfig.js +33 -0
  88. package/build/ts/src/queryManagement/validateWorkspaceConfig.js.map +1 -0
  89. package/build/ts/src/version.d.ts +1 -1
  90. package/build/ts/src/version.js +1 -1
  91. package/build/yasgui.min.css +10 -1
  92. package/build/yasgui.min.css.map +3 -3
  93. package/build/yasgui.min.js +398 -172
  94. package/build/yasgui.min.js.map +4 -4
  95. package/package.json +1 -1
  96. package/src/PersistentConfig.ts +61 -0
  97. package/src/Tab.ts +431 -20
  98. package/src/TabContextMenu.ts +10 -0
  99. package/src/TabElements.scss +46 -7
  100. package/src/TabElements.ts +95 -27
  101. package/src/TabSettingsModal.scss +102 -5
  102. package/src/TabSettingsModal.ts +48 -25
  103. package/src/endpointSelect.scss +2 -3
  104. package/src/index.scss +4 -0
  105. package/src/index.ts +7 -0
  106. package/src/queryManagement/QueryBrowser.scss +418 -0
  107. package/src/queryManagement/QueryBrowser.ts +1079 -0
  108. package/src/queryManagement/SaveManagedQueryModal.scss +245 -0
  109. package/src/queryManagement/SaveManagedQueryModal.ts +554 -0
  110. package/src/queryManagement/WorkspaceSettingsForm.ts +546 -0
  111. package/src/queryManagement/backends/BaseGitProviderClient.ts +124 -0
  112. package/src/queryManagement/backends/BitbucketProviderClient.ts +403 -0
  113. package/src/queryManagement/backends/GitWorkspaceBackend.ts +96 -0
  114. package/src/queryManagement/backends/GiteaProviderClient.ts +316 -0
  115. package/src/queryManagement/backends/GithubProviderClient.ts +319 -0
  116. package/src/queryManagement/backends/GitlabProviderClient.ts +327 -0
  117. package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +175 -0
  118. package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +729 -0
  119. package/src/queryManagement/backends/WorkspaceBackend.ts +41 -0
  120. package/src/queryManagement/backends/errors.ts +28 -0
  121. package/src/queryManagement/backends/getWorkspaceBackend.ts +132 -0
  122. package/src/queryManagement/backends/gitRemote.ts +54 -0
  123. package/src/queryManagement/browserFilter.ts +8 -0
  124. package/src/queryManagement/index.ts +15 -0
  125. package/src/queryManagement/normalizeQueryFilename.ts +8 -0
  126. package/src/queryManagement/openManagedQuery.ts +31 -0
  127. package/src/queryManagement/saveManagedQuery.ts +135 -0
  128. package/src/queryManagement/textHash.ts +15 -0
  129. package/src/queryManagement/types.ts +85 -0
  130. package/src/queryManagement/validateWorkspaceConfig.ts +40 -0
  131. package/src/tab.scss +4 -14
  132. package/src/themes.scss +14 -23
  133. package/src/version.ts +1 -1
@@ -0,0 +1,403 @@
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 BitbucketMetaEntry = {
7
+ type: "commit_directory" | "commit_file";
8
+ path: string;
9
+ };
10
+
11
+ type BitbucketMetaResponse = {
12
+ values?: BitbucketMetaEntry[];
13
+ next?: string;
14
+ };
15
+
16
+ type BitbucketCommit = {
17
+ hash: string;
18
+ date?: string;
19
+ message?: string;
20
+ author?: {
21
+ user?: { display_name?: string };
22
+ raw?: string;
23
+ };
24
+ };
25
+
26
+ type BitbucketCommitsResponse = {
27
+ values?: BitbucketCommit[];
28
+ };
29
+
30
+ type BitbucketRepoResponse = {
31
+ mainbranch?: { name?: string };
32
+ };
33
+
34
+ function inferApiBase(config: GitWorkspaceConfig): string {
35
+ const configured = (config as any).apiBaseUrl as string | undefined;
36
+ if (configured?.trim()) return configured.trim().replace(/\/+$/g, "");
37
+ return "https://api.bitbucket.org/2.0";
38
+ }
39
+
40
+ function parseWorkspaceRepo(remoteUrl: string): { host: string; workspace: string; repo: string } {
41
+ const { host, repoPath } = parseGitRemote(remoteUrl);
42
+ const parts = repoPath.split("/").filter(Boolean);
43
+ if (parts.length < 2) throw new Error("Invalid Bitbucket remote URL");
44
+ const workspace = parts[0];
45
+ const repo = parts[1];
46
+ return { host, workspace, repo };
47
+ }
48
+
49
+ export class BitbucketProviderClient extends BaseGitProviderClient {
50
+ public static canHandle(config: GitWorkspaceConfig): boolean {
51
+ const provider = (config as any).provider as string | undefined;
52
+ if (provider && provider !== "auto") return provider === "bitbucket";
53
+
54
+ try {
55
+ const { host } = parseGitRemote(config.remoteUrl);
56
+ return host === "bitbucket.org";
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ private async requestJson<T>(
63
+ apiBase: string,
64
+ config: GitWorkspaceConfig,
65
+ path: string,
66
+ init?: RequestInit,
67
+ ): Promise<{ status: number; json?: T }> {
68
+ const headers: Record<string, string> = {
69
+ Accept: "application/json",
70
+ };
71
+
72
+ // Bitbucket Cloud uses basic auth: username + app password.
73
+ const username = config.auth.username?.trim();
74
+ const token = config.auth.token?.trim();
75
+ if (username && token) headers.Authorization = `Basic ${this.base64Encode(`${username}:${token}`)}`;
76
+
77
+ const url = path.startsWith("http") ? path : `${apiBase}${path}`;
78
+ const res = await fetch(url, {
79
+ ...init,
80
+ // Avoid stale results after create/delete/rename.
81
+ cache: "no-store",
82
+ headers: {
83
+ ...headers,
84
+ ...(init?.headers as any),
85
+ },
86
+ });
87
+
88
+ const status = res.status;
89
+ const contentType = res.headers.get("content-type") || "";
90
+ if (contentType.includes("application/json")) {
91
+ const text = await res.text();
92
+ if (text.trim()) {
93
+ try {
94
+ const json = JSON.parse(text) as T;
95
+ return { status, json };
96
+ } catch {
97
+ // Invalid JSON, return status only
98
+ return { status };
99
+ }
100
+ }
101
+ }
102
+
103
+ return { status };
104
+ }
105
+
106
+ private async requestText(
107
+ apiBase: string,
108
+ config: GitWorkspaceConfig,
109
+ url: string,
110
+ init?: RequestInit,
111
+ ): Promise<{ status: number; text: string }> {
112
+ const headers: Record<string, string> = {
113
+ Accept: "text/plain",
114
+ };
115
+
116
+ const username = config.auth.username?.trim();
117
+ const token = config.auth.token?.trim();
118
+ if (username && token) headers.Authorization = `Basic ${this.base64Encode(`${username}:${token}`)}`;
119
+
120
+ const res = await fetch(url.startsWith("http") ? url : `${apiBase}${url}`, {
121
+ ...init,
122
+ // Avoid stale results after create/delete/rename.
123
+ cache: "no-store",
124
+ headers: {
125
+ ...headers,
126
+ ...(init?.headers as any),
127
+ },
128
+ });
129
+
130
+ return { status: res.status, text: await res.text() };
131
+ }
132
+
133
+ async validateAccess(config: GitWorkspaceConfig): Promise<void> {
134
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
135
+ if (host !== "bitbucket.org") {
136
+ throw new WorkspaceBackendError(
137
+ "UNKNOWN",
138
+ "This Bitbucket provider currently supports Bitbucket Cloud (bitbucket.org) only.",
139
+ );
140
+ }
141
+
142
+ const apiBase = inferApiBase(config);
143
+ const username = config.auth.username?.trim();
144
+ if (!username) {
145
+ throw new WorkspaceBackendError(
146
+ "UNKNOWN",
147
+ "Bitbucket Cloud requires a username (workspace/user) in addition to an app password token.",
148
+ );
149
+ }
150
+
151
+ const { status } = await this.requestJson(
152
+ apiBase,
153
+ config,
154
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}`,
155
+ );
156
+ this.ensureOk(status, "Could not access Bitbucket repository with the provided credentials.");
157
+ }
158
+
159
+ private async resolveBranch(
160
+ apiBase: string,
161
+ config: GitWorkspaceConfig,
162
+ workspace: string,
163
+ repo: string,
164
+ ): Promise<string> {
165
+ const configured = config.branch?.trim();
166
+ if (configured) return configured;
167
+
168
+ const { status, json } = await this.requestJson<BitbucketRepoResponse>(
169
+ apiBase,
170
+ config,
171
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}`,
172
+ );
173
+ this.ensureOk(status, "Failed to resolve repository default branch.");
174
+
175
+ const inferred = json?.mainbranch?.name?.trim();
176
+ if (inferred) return inferred;
177
+
178
+ // Fallback: common default.
179
+ return "main";
180
+ }
181
+
182
+ async listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<FolderEntry[]> {
183
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
184
+ if (host !== "bitbucket.org") return [];
185
+
186
+ const apiBase = inferApiBase(config);
187
+
188
+ const relPath = folderId?.trim() || "";
189
+ const fullPath = this.joinPath(config.rootPath, relPath);
190
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
191
+
192
+ // Use the /src endpoint in meta mode to list directory.
193
+ const basePath =
194
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/src/${encodeURIComponent(ref)}` +
195
+ (fullPath ? `/${this.encodePath(fullPath)}` : "");
196
+
197
+ const entries: FolderEntry[] = [];
198
+ let nextUrl: string | undefined = `${apiBase}${basePath}?format=meta&pagelen=100`;
199
+
200
+ for (let guard = 0; guard < 50 && nextUrl; guard++) {
201
+ const response: { status: number; json?: BitbucketMetaResponse } = await this.requestJson<BitbucketMetaResponse>(
202
+ apiBase,
203
+ config,
204
+ nextUrl,
205
+ );
206
+ const status = response.status;
207
+ const json: BitbucketMetaResponse | undefined = response.json;
208
+ if (status === 404) return [];
209
+ this.ensureOk(status, "Failed to list folder contents.");
210
+
211
+ const values = json?.values || [];
212
+ for (const v of values) {
213
+ const name = v.path.split("/").filter(Boolean).pop() || v.path;
214
+ if (v.type === "commit_directory") {
215
+ const id = relPath ? this.joinPath(relPath, name) : name;
216
+ entries.push({ kind: "folder", id, label: name, parentId: relPath || undefined });
217
+ }
218
+
219
+ if (v.type === "commit_file") {
220
+ if (!/\.sparql$/i.test(name)) continue;
221
+ const id = relPath ? this.joinPath(relPath, name) : name;
222
+ const label = name.replace(/\.sparql$/i, "");
223
+ entries.push({ kind: "query", id, label, parentId: relPath || undefined });
224
+ }
225
+ }
226
+
227
+ nextUrl = json?.next;
228
+ }
229
+
230
+ entries.sort((a, b) => {
231
+ if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
232
+ return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
233
+ });
234
+
235
+ return entries;
236
+ }
237
+
238
+ private async getLatestCommitHash(
239
+ apiBase: string,
240
+ config: GitWorkspaceConfig,
241
+ workspace: string,
242
+ repo: string,
243
+ filePath: string,
244
+ ): Promise<string | undefined> {
245
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
246
+ const qs = new URLSearchParams();
247
+ qs.set("path", filePath);
248
+ qs.set("pagelen", "1");
249
+
250
+ const { status, json } = await this.requestJson<BitbucketCommitsResponse>(
251
+ apiBase,
252
+ config,
253
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref)}?${qs.toString()}`,
254
+ );
255
+
256
+ if (status === 404) return undefined;
257
+ this.ensureOk(status, "Failed to read query metadata.");
258
+
259
+ const commits = json?.values || [];
260
+ return commits[0]?.hash;
261
+ }
262
+
263
+ async readQuery(config: GitWorkspaceConfig, queryId: string): Promise<ReadResult> {
264
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
265
+ if (host !== "bitbucket.org") throw new WorkspaceBackendError("NOT_FOUND", "Unsupported Bitbucket host");
266
+
267
+ const apiBase = inferApiBase(config);
268
+ const filePath = this.joinPath(config.rootPath, queryId);
269
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
270
+
271
+ const { status, text } = await this.requestText(
272
+ apiBase,
273
+ config,
274
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/src/${encodeURIComponent(ref)}/${this.encodePath(filePath)}`,
275
+ );
276
+
277
+ this.ensureOk(status, "Failed to read query.");
278
+
279
+ const versionTag = await this.getLatestCommitHash(apiBase, config, workspace, repo, filePath);
280
+ return { queryText: text, versionTag };
281
+ }
282
+
283
+ async writeQuery(
284
+ config: GitWorkspaceConfig,
285
+ queryId: string,
286
+ queryText: string,
287
+ options?: WriteQueryOptions,
288
+ ): Promise<void> {
289
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
290
+ if (host !== "bitbucket.org") throw new WorkspaceBackendError("UNKNOWN", "Unsupported Bitbucket host");
291
+
292
+ const apiBase = inferApiBase(config);
293
+
294
+ const filePath = this.joinPath(config.rootPath, queryId);
295
+ const current = await this.getLatestCommitHash(apiBase, config, workspace, repo, filePath);
296
+
297
+ if (options?.expectedVersionTag && current && options.expectedVersionTag !== current) {
298
+ throw new WorkspaceBackendError(
299
+ "CONFLICT",
300
+ "The file changed remotely since it was last opened. Please reload the managed query and try saving again.",
301
+ );
302
+ }
303
+
304
+ const message = this.getCommitMessage(queryId, options, !current);
305
+
306
+ const form = new FormData();
307
+ form.append("message", message);
308
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
309
+ if (ref) form.append("branch", ref);
310
+
311
+ // Bitbucket expects file content in a multipart field named by its path.
312
+ form.append(filePath, new Blob([queryText], { type: "text/plain" }));
313
+
314
+ const { status } = await this.requestJson(
315
+ apiBase,
316
+ config,
317
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/src`,
318
+ {
319
+ method: "POST",
320
+ body: form as any,
321
+ },
322
+ );
323
+
324
+ this.ensureOk(status, "Failed to save query to Bitbucket workspace.");
325
+ }
326
+
327
+ async listVersions(config: GitWorkspaceConfig, queryId: string): Promise<VersionInfo[]> {
328
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
329
+ if (host !== "bitbucket.org") return [];
330
+
331
+ const apiBase = inferApiBase(config);
332
+
333
+ const filePath = this.joinPath(config.rootPath, queryId);
334
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
335
+
336
+ const qs = new URLSearchParams();
337
+ qs.set("path", filePath);
338
+ qs.set("pagelen", "30");
339
+
340
+ const { status, json } = await this.requestJson<BitbucketCommitsResponse>(
341
+ apiBase,
342
+ config,
343
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref)}?${qs.toString()}`,
344
+ );
345
+
346
+ this.ensureOk(status, "Failed to list query versions.");
347
+
348
+ const commits = json?.values || [];
349
+ return commits
350
+ .map((c) => {
351
+ const createdAt = c.date || new Date().toISOString();
352
+ const author = c.author?.user?.display_name || c.author?.raw;
353
+ return { id: c.hash, createdAt, author, message: c.message } satisfies VersionInfo;
354
+ })
355
+ .filter((v) => !!v.id);
356
+ }
357
+
358
+ async readVersion(config: GitWorkspaceConfig, queryId: string, versionId: string): Promise<ReadResult> {
359
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
360
+ if (host !== "bitbucket.org") throw new WorkspaceBackendError("NOT_FOUND", "Unsupported Bitbucket host");
361
+
362
+ const apiBase = inferApiBase(config);
363
+
364
+ const filePath = this.joinPath(config.rootPath, queryId);
365
+
366
+ const { status, text } = await this.requestText(
367
+ apiBase,
368
+ config,
369
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/src/${encodeURIComponent(versionId)}/${this.encodePath(filePath)}`,
370
+ );
371
+
372
+ this.ensureOk(status, "Failed to read query version.");
373
+
374
+ return { queryText: text, versionTag: versionId };
375
+ }
376
+
377
+ async deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void> {
378
+ const { host, workspace, repo } = parseWorkspaceRepo(config.remoteUrl);
379
+ if (host !== "bitbucket.org") throw new WorkspaceBackendError("UNKNOWN", "Unsupported Bitbucket host");
380
+
381
+ const apiBase = inferApiBase(config);
382
+
383
+ const filePath = this.joinPath(config.rootPath, queryId);
384
+ const ref = await this.resolveBranch(apiBase, config, workspace, repo);
385
+
386
+ const form = new FormData();
387
+ form.append("message", this.getDeleteMessage(queryId));
388
+ if (ref) form.append("branch", ref);
389
+ // Bitbucket deletes files by sending one or more `files` fields.
390
+ form.append("files", filePath);
391
+
392
+ const { status } = await this.requestJson(
393
+ apiBase,
394
+ config,
395
+ `/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/src`,
396
+ { method: "POST", body: form as any },
397
+ );
398
+
399
+ // If the file doesn't exist, Bitbucket may return 404.
400
+ if (status === 404) return;
401
+ this.ensureOk(status, "Failed to delete query from Bitbucket workspace.");
402
+ }
403
+ }
@@ -0,0 +1,96 @@
1
+ import type { GitWorkspaceConfig, FolderEntry, ReadResult, VersionInfo, WriteQueryOptions } from "../types";
2
+ import type { WorkspaceBackend } from "./WorkspaceBackend";
3
+ import { WorkspaceBackendError } from "./errors";
4
+ import { normalizeQueryFilename } from "../normalizeQueryFilename";
5
+
6
+ export interface GitProviderClient {
7
+ validateAccess(config: GitWorkspaceConfig): Promise<void>;
8
+ listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<FolderEntry[]>;
9
+ readQuery(config: GitWorkspaceConfig, queryId: string): Promise<ReadResult>;
10
+ writeQuery(
11
+ config: GitWorkspaceConfig,
12
+ queryId: string,
13
+ queryText: string,
14
+ options?: WriteQueryOptions,
15
+ ): Promise<void>;
16
+ listVersions(config: GitWorkspaceConfig, queryId: string): Promise<VersionInfo[]>;
17
+ readVersion(config: GitWorkspaceConfig, queryId: string, versionId: string): Promise<ReadResult>;
18
+
19
+ deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void>;
20
+ }
21
+
22
+ export default class GitWorkspaceBackend implements WorkspaceBackend {
23
+ public readonly type = "git" as const;
24
+
25
+ constructor(
26
+ private config: GitWorkspaceConfig,
27
+ private client?: GitProviderClient,
28
+ ) {}
29
+
30
+ private missingClientError(): WorkspaceBackendError {
31
+ return new WorkspaceBackendError(
32
+ "UNKNOWN",
33
+ `No GitProviderClient configured for remote '${this.config.remoteUrl}'. Supported providers: GitHub, GitLab, Bitbucket Cloud (bitbucket.org), and Gitea. For self-hosted/enterprise instances, set git workspace 'provider' and/or 'apiBaseUrl'.`,
34
+ );
35
+ }
36
+
37
+ async validateAccess(): Promise<void> {
38
+ if (!this.client) throw this.missingClientError();
39
+ return this.client.validateAccess(this.config);
40
+ }
41
+
42
+ async listFolder(_folderId?: string): Promise<FolderEntry[]> {
43
+ if (!this.client) throw this.missingClientError();
44
+ return this.client.listFolder(this.config, _folderId);
45
+ }
46
+
47
+ async readQuery(_queryId: string): Promise<ReadResult> {
48
+ if (!this.client) throw this.missingClientError();
49
+ return this.client.readQuery(this.config, _queryId);
50
+ }
51
+
52
+ async writeQuery(_queryId: string, _queryText: string, _options?: WriteQueryOptions): Promise<void> {
53
+ if (!this.client) throw this.missingClientError();
54
+ return this.client.writeQuery(this.config, _queryId, _queryText, _options);
55
+ }
56
+
57
+ async listVersions(_queryId: string): Promise<VersionInfo[]> {
58
+ if (!this.client) throw this.missingClientError();
59
+ return this.client.listVersions(this.config, _queryId);
60
+ }
61
+
62
+ async readVersion(_queryId: string, _versionId: string): Promise<ReadResult> {
63
+ if (!this.client) throw this.missingClientError();
64
+ return this.client.readVersion(this.config, _queryId, _versionId);
65
+ }
66
+
67
+ async deleteQuery(_queryId: string): Promise<void> {
68
+ if (!this.client) throw this.missingClientError();
69
+ return this.client.deleteQuery(this.config, _queryId);
70
+ }
71
+
72
+ async renameQuery(queryId: string, newLabel: string): Promise<void> {
73
+ if (!this.client) throw this.missingClientError();
74
+
75
+ const next = (newLabel || "").trim();
76
+ if (!next) throw new WorkspaceBackendError("UNKNOWN", "New name is required");
77
+
78
+ const parts = queryId.split("/").filter(Boolean);
79
+ const oldFilename = parts.pop() || queryId;
80
+ const folderPrefix = parts.join("/");
81
+
82
+ const safe = next.replace(/[\\/]/g, "-");
83
+ const newFilename = normalizeQueryFilename(safe);
84
+ const newPath = folderPrefix ? `${folderPrefix}/${newFilename}` : newFilename;
85
+
86
+ if (newPath === queryId) return;
87
+
88
+ const read = await this.client.readQuery(this.config, queryId);
89
+
90
+ await this.client.writeQuery(this.config, newPath, read.queryText, {
91
+ message: `Rename ${oldFilename} to ${newFilename}`,
92
+ });
93
+
94
+ await this.client.deleteQuery(this.config, queryId);
95
+ }
96
+ }