@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,316 @@
|
|
|
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 GiteaContentItem = {
|
|
7
|
+
type: "file" | "dir";
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
sha?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
encoding?: "base64";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type GiteaCommit = {
|
|
16
|
+
sha: string;
|
|
17
|
+
created?: string;
|
|
18
|
+
commit?: {
|
|
19
|
+
message?: string;
|
|
20
|
+
author?: { name?: string; date?: string };
|
|
21
|
+
committer?: { name?: string; date?: string };
|
|
22
|
+
};
|
|
23
|
+
author?: { login?: string };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function inferApiBase(config: GitWorkspaceConfig, host: string): string {
|
|
27
|
+
const configured = (config as any).apiBaseUrl as string | undefined;
|
|
28
|
+
if (configured?.trim()) return configured.trim().replace(/\/+$/g, "");
|
|
29
|
+
return `https://${host}/api/v1`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseOwnerRepo(remoteUrl: string): { owner: string; repo: string; host: string } {
|
|
33
|
+
const { host, repoPath } = parseGitRemote(remoteUrl);
|
|
34
|
+
const parts = repoPath.split("/").filter(Boolean);
|
|
35
|
+
if (parts.length < 2) throw new Error("Invalid git remote URL");
|
|
36
|
+
const owner = parts[0];
|
|
37
|
+
const repo = parts[1];
|
|
38
|
+
return { owner, repo, host };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class GiteaProviderClient extends BaseGitProviderClient {
|
|
42
|
+
public static canHandle(config: GitWorkspaceConfig): boolean {
|
|
43
|
+
const provider = (config as any).provider as string | undefined;
|
|
44
|
+
if (provider && provider !== "auto") return provider === "gitea";
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const { host } = parseGitRemote(config.remoteUrl);
|
|
48
|
+
return host.includes("gitea");
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async request<T>(
|
|
55
|
+
apiBase: string,
|
|
56
|
+
config: GitWorkspaceConfig,
|
|
57
|
+
path: string,
|
|
58
|
+
init?: RequestInit,
|
|
59
|
+
): Promise<{ status: number; json?: T; text?: string }> {
|
|
60
|
+
const headers: Record<string, string> = {
|
|
61
|
+
Accept: "application/json",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const token = config.auth.token?.trim();
|
|
65
|
+
if (token) headers.Authorization = `token ${token}`;
|
|
66
|
+
|
|
67
|
+
const res = await fetch(`${apiBase}${path}`, {
|
|
68
|
+
...init,
|
|
69
|
+
// Avoid stale results after create/delete/rename.
|
|
70
|
+
cache: "no-store",
|
|
71
|
+
headers: {
|
|
72
|
+
...headers,
|
|
73
|
+
...(init?.headers as any),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const status = res.status;
|
|
78
|
+
const contentType = res.headers.get("content-type") || "";
|
|
79
|
+
|
|
80
|
+
if (contentType.includes("application/json")) {
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (text.trim()) {
|
|
83
|
+
try {
|
|
84
|
+
const json = JSON.parse(text) as T;
|
|
85
|
+
return { status, json };
|
|
86
|
+
} catch {
|
|
87
|
+
// Invalid JSON, return status only
|
|
88
|
+
return { status };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { status };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
return { status, text };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async validateAccess(config: GitWorkspaceConfig): Promise<void> {
|
|
99
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
100
|
+
const apiBase = inferApiBase(config, host);
|
|
101
|
+
|
|
102
|
+
const { status } = await this.request(
|
|
103
|
+
apiBase,
|
|
104
|
+
config,
|
|
105
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
|
|
106
|
+
);
|
|
107
|
+
this.ensureOk(status, "Could not access repository with the provided token.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<FolderEntry[]> {
|
|
111
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
112
|
+
const apiBase = inferApiBase(config, host);
|
|
113
|
+
|
|
114
|
+
const relPath = folderId?.trim() || "";
|
|
115
|
+
const fullPath = this.joinPath(config.rootPath, relPath);
|
|
116
|
+
|
|
117
|
+
const pathPart = fullPath ? `/${this.encodePath(fullPath)}` : "";
|
|
118
|
+
const ref = config.branch?.trim() ? `?ref=${encodeURIComponent(config.branch.trim())}` : "";
|
|
119
|
+
|
|
120
|
+
const { status, json } = await this.request<GiteaContentItem[] | GiteaContentItem>(
|
|
121
|
+
apiBase,
|
|
122
|
+
config,
|
|
123
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents${pathPart}${ref}`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (status === 404) return [];
|
|
127
|
+
this.ensureOk(status, "Failed to list folder contents.");
|
|
128
|
+
|
|
129
|
+
const items = Array.isArray(json) ? json : [];
|
|
130
|
+
|
|
131
|
+
const entries: FolderEntry[] = [];
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
if (item.type === "dir") {
|
|
134
|
+
const id = relPath ? this.joinPath(relPath, item.name) : item.name;
|
|
135
|
+
entries.push({ kind: "folder", id, label: item.name, parentId: relPath || undefined });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (item.type === "file") {
|
|
140
|
+
if (!/\.sparql$/i.test(item.name)) continue;
|
|
141
|
+
const id = relPath ? this.joinPath(relPath, item.name) : item.name;
|
|
142
|
+
const label = item.name.replace(/\.sparql$/i, "");
|
|
143
|
+
entries.push({ kind: "query", id, label, parentId: relPath || undefined });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
entries.sort((a, b) => {
|
|
148
|
+
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
149
|
+
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return entries;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async readFileAtRef(
|
|
156
|
+
config: GitWorkspaceConfig,
|
|
157
|
+
apiBase: string,
|
|
158
|
+
owner: string,
|
|
159
|
+
repo: string,
|
|
160
|
+
queryId: string,
|
|
161
|
+
ref?: string,
|
|
162
|
+
): Promise<{ text: string; sha?: string }> {
|
|
163
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
164
|
+
const refQ = ref
|
|
165
|
+
? `?ref=${encodeURIComponent(ref)}`
|
|
166
|
+
: config.branch?.trim()
|
|
167
|
+
? `?ref=${encodeURIComponent(config.branch.trim())}`
|
|
168
|
+
: "";
|
|
169
|
+
|
|
170
|
+
const { status, json } = await this.request<GiteaContentItem>(
|
|
171
|
+
apiBase,
|
|
172
|
+
config,
|
|
173
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${this.encodePath(filePath)}${refQ}`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
this.ensureOk(status, "Failed to read query.");
|
|
177
|
+
|
|
178
|
+
const content = json?.content || "";
|
|
179
|
+
const text = json?.encoding === "base64" ? this.base64DecodeUtf8(content) : content;
|
|
180
|
+
return { text, sha: json?.sha };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async readQuery(config: GitWorkspaceConfig, queryId: string): Promise<ReadResult> {
|
|
184
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
185
|
+
const apiBase = inferApiBase(config, host);
|
|
186
|
+
|
|
187
|
+
const { text, sha } = await this.readFileAtRef(config, apiBase, owner, repo, queryId);
|
|
188
|
+
return { queryText: text, versionTag: sha };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async writeQuery(
|
|
192
|
+
config: GitWorkspaceConfig,
|
|
193
|
+
queryId: string,
|
|
194
|
+
queryText: string,
|
|
195
|
+
options?: WriteQueryOptions,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
198
|
+
const apiBase = inferApiBase(config, host);
|
|
199
|
+
|
|
200
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
201
|
+
|
|
202
|
+
let currentSha: string | undefined;
|
|
203
|
+
try {
|
|
204
|
+
const res = await this.readFileAtRef(config, apiBase, owner, repo, queryId);
|
|
205
|
+
currentSha = res.sha;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const err = e as any;
|
|
208
|
+
if (err?.code !== "NOT_FOUND") throw e;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options?.expectedVersionTag && currentSha && options.expectedVersionTag !== currentSha) {
|
|
212
|
+
throw new WorkspaceBackendError(
|
|
213
|
+
"CONFLICT",
|
|
214
|
+
"The file changed remotely since it was last opened. Please reload the managed query and try saving again.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const message = this.getCommitMessage(queryId, options, !currentSha);
|
|
219
|
+
const body: any = {
|
|
220
|
+
message,
|
|
221
|
+
content: this.base64EncodeUtf8(queryText),
|
|
222
|
+
};
|
|
223
|
+
if (config.branch?.trim()) body.branch = config.branch.trim();
|
|
224
|
+
if (currentSha) body.sha = currentSha;
|
|
225
|
+
|
|
226
|
+
const { status } = await this.request(
|
|
227
|
+
apiBase,
|
|
228
|
+
config,
|
|
229
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${this.encodePath(filePath)}`,
|
|
230
|
+
{
|
|
231
|
+
method: "PUT",
|
|
232
|
+
body: JSON.stringify(body),
|
|
233
|
+
headers: { "Content-Type": "application/json" },
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
this.ensureOk(status, "Failed to save query to Gitea workspace.");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async listVersions(config: GitWorkspaceConfig, queryId: string): Promise<VersionInfo[]> {
|
|
241
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
242
|
+
const apiBase = inferApiBase(config, host);
|
|
243
|
+
|
|
244
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
245
|
+
|
|
246
|
+
const qs = new URLSearchParams();
|
|
247
|
+
if (config.branch?.trim()) qs.set("sha", config.branch.trim());
|
|
248
|
+
qs.set("path", filePath);
|
|
249
|
+
qs.set("limit", "30");
|
|
250
|
+
|
|
251
|
+
const { status, json } = await this.request<GiteaCommit[]>(
|
|
252
|
+
apiBase,
|
|
253
|
+
config,
|
|
254
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?${qs.toString()}`,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
this.ensureOk(status, "Failed to list query versions.");
|
|
258
|
+
|
|
259
|
+
const commits = Array.isArray(json) ? json : [];
|
|
260
|
+
return commits
|
|
261
|
+
.map((c) => {
|
|
262
|
+
const createdAt = c.created || c.commit?.committer?.date || c.commit?.author?.date || new Date().toISOString();
|
|
263
|
+
const author = c.author?.login || c.commit?.author?.name;
|
|
264
|
+
const message = c.commit?.message || "";
|
|
265
|
+
return { id: c.sha, createdAt, author, message } satisfies VersionInfo;
|
|
266
|
+
})
|
|
267
|
+
.filter((v) => !!v.id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async readVersion(config: GitWorkspaceConfig, queryId: string, versionId: string): Promise<ReadResult> {
|
|
271
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
272
|
+
const apiBase = inferApiBase(config, host);
|
|
273
|
+
|
|
274
|
+
const { text, sha } = await this.readFileAtRef(config, apiBase, owner, repo, queryId, versionId);
|
|
275
|
+
return { queryText: text, versionTag: sha };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void> {
|
|
279
|
+
const { owner, repo, host } = parseOwnerRepo(config.remoteUrl);
|
|
280
|
+
const apiBase = inferApiBase(config, host);
|
|
281
|
+
|
|
282
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
283
|
+
|
|
284
|
+
let currentSha: string | undefined;
|
|
285
|
+
try {
|
|
286
|
+
const res = await this.readFileAtRef(config, apiBase, owner, repo, queryId);
|
|
287
|
+
currentSha = res.sha;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
const err = e as any;
|
|
290
|
+
if (err?.code === "NOT_FOUND") return;
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!currentSha) return;
|
|
295
|
+
|
|
296
|
+
const body: any = {
|
|
297
|
+
message: this.getDeleteMessage(queryId),
|
|
298
|
+
sha: currentSha,
|
|
299
|
+
};
|
|
300
|
+
if (config.branch?.trim()) body.branch = config.branch.trim();
|
|
301
|
+
|
|
302
|
+
const { status } = await this.request(
|
|
303
|
+
apiBase,
|
|
304
|
+
config,
|
|
305
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${this.encodePath(filePath)}`,
|
|
306
|
+
{
|
|
307
|
+
method: "DELETE",
|
|
308
|
+
body: JSON.stringify(body),
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (status === 404) return;
|
|
314
|
+
this.ensureOk(status, "Failed to delete query.");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
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 GithubContentItem =
|
|
7
|
+
| {
|
|
8
|
+
type: "file";
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
type: "dir";
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type GithubContentResponse =
|
|
19
|
+
| {
|
|
20
|
+
type: "file";
|
|
21
|
+
name: string;
|
|
22
|
+
path: string;
|
|
23
|
+
sha: string;
|
|
24
|
+
content: string;
|
|
25
|
+
encoding: "base64";
|
|
26
|
+
}
|
|
27
|
+
| GithubContentItem[];
|
|
28
|
+
|
|
29
|
+
type GithubCommit = {
|
|
30
|
+
sha: string;
|
|
31
|
+
commit: {
|
|
32
|
+
committer?: { date?: string };
|
|
33
|
+
author?: { date?: string };
|
|
34
|
+
message?: string;
|
|
35
|
+
};
|
|
36
|
+
author?: { login?: string };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function isGitHubRemote(remoteUrl: string): boolean {
|
|
40
|
+
try {
|
|
41
|
+
const { host } = parseGitRemote(remoteUrl);
|
|
42
|
+
return host === "github.com" || host.includes("github");
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function inferApiBase(config: GitWorkspaceConfig, host: string): string {
|
|
49
|
+
const configured = (config as any).apiBaseUrl as string | undefined;
|
|
50
|
+
if (configured?.trim()) return configured.trim().replace(/\/+$/g, "");
|
|
51
|
+
if (host === "github.com") return "https://api.github.com";
|
|
52
|
+
// GitHub Enterprise default
|
|
53
|
+
return `https://${host}/api/v3`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseOwnerRepo(remoteUrl: string): { owner: string; repo: string; host: string } {
|
|
57
|
+
const { host, repoPath } = parseGitRemote(remoteUrl);
|
|
58
|
+
const parts = repoPath.split("/").filter(Boolean);
|
|
59
|
+
if (parts.length < 2) throw new Error("Invalid git remote URL");
|
|
60
|
+
const owner = parts[0];
|
|
61
|
+
const repo = parts[1];
|
|
62
|
+
if (!owner || !repo) throw new Error("Invalid git remote URL");
|
|
63
|
+
return { owner, repo, host };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class GithubProviderClient extends BaseGitProviderClient {
|
|
67
|
+
public static canHandle(config: GitWorkspaceConfig): boolean {
|
|
68
|
+
const provider = (config as any).provider as string | undefined;
|
|
69
|
+
if (provider && provider !== "auto") return provider === "github";
|
|
70
|
+
return isGitHubRemote(config.remoteUrl);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async request<T>(
|
|
74
|
+
config: GitWorkspaceConfig,
|
|
75
|
+
path: string,
|
|
76
|
+
init?: RequestInit,
|
|
77
|
+
): Promise<{ status: number; json?: T }> {
|
|
78
|
+
const headers: Record<string, string> = {
|
|
79
|
+
Accept: "application/vnd.github+json",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const token = config.auth.token?.trim();
|
|
83
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
84
|
+
|
|
85
|
+
const { host } = parseOwnerRepo(config.remoteUrl);
|
|
86
|
+
const apiBase = inferApiBase(config, host);
|
|
87
|
+
|
|
88
|
+
const res = await fetch(`${apiBase}${path}`, {
|
|
89
|
+
...init,
|
|
90
|
+
// Avoid stale results after create/delete/rename.
|
|
91
|
+
cache: "no-store",
|
|
92
|
+
headers: {
|
|
93
|
+
...headers,
|
|
94
|
+
...(init?.headers as any),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const status = res.status;
|
|
99
|
+
const contentType = res.headers.get("content-type") || "";
|
|
100
|
+
if (contentType.includes("application/json")) {
|
|
101
|
+
const text = await res.text();
|
|
102
|
+
if (text.trim()) {
|
|
103
|
+
try {
|
|
104
|
+
const json = JSON.parse(text) as T;
|
|
105
|
+
return { status, json };
|
|
106
|
+
} catch {
|
|
107
|
+
// Invalid JSON, return status only
|
|
108
|
+
return { status };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { status };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async validateAccess(config: GitWorkspaceConfig): Promise<void> {
|
|
117
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
118
|
+
const { status } = await this.request(config, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
|
|
119
|
+
this.ensureOk(status, "Could not access repository with the provided token.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<FolderEntry[]> {
|
|
123
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
124
|
+
const relPath = folderId?.trim() || "";
|
|
125
|
+
const fullPath = this.joinPath(config.rootPath, relPath);
|
|
126
|
+
|
|
127
|
+
const pathPart = fullPath ? `/${this.encodePath(fullPath)}` : "";
|
|
128
|
+
const ref = config.branch?.trim() ? `?ref=${encodeURIComponent(config.branch.trim())}` : "";
|
|
129
|
+
|
|
130
|
+
const { status, json } = await this.request<GithubContentResponse>(
|
|
131
|
+
config,
|
|
132
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents${pathPart}${ref}`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (status === 404) return [];
|
|
136
|
+
this.ensureOk(status, "Failed to list folder contents.");
|
|
137
|
+
|
|
138
|
+
if (!Array.isArray(json)) return [];
|
|
139
|
+
|
|
140
|
+
const entries: FolderEntry[] = [];
|
|
141
|
+
for (const item of json) {
|
|
142
|
+
if (item.type === "dir") {
|
|
143
|
+
const id = config.rootPath
|
|
144
|
+
? item.path.slice(this.joinPath(config.rootPath).length).replace(/^\//, "")
|
|
145
|
+
: item.path;
|
|
146
|
+
entries.push({ kind: "folder", id, label: item.name, parentId: relPath || undefined });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (item.type === "file") {
|
|
151
|
+
if (!/\.sparql$/i.test(item.name)) continue;
|
|
152
|
+
const id = config.rootPath
|
|
153
|
+
? item.path.slice(this.joinPath(config.rootPath).length).replace(/^\//, "")
|
|
154
|
+
: item.path;
|
|
155
|
+
const label = item.name.replace(/\.sparql$/i, "");
|
|
156
|
+
entries.push({ kind: "query", id, label, parentId: relPath || undefined });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Keep stable sort: folders first then queries.
|
|
161
|
+
entries.sort((a, b) => {
|
|
162
|
+
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
163
|
+
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async readFileAtRef(
|
|
170
|
+
config: GitWorkspaceConfig,
|
|
171
|
+
queryId: string,
|
|
172
|
+
ref?: string,
|
|
173
|
+
): Promise<{ text: string; sha: string }> {
|
|
174
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
175
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
176
|
+
|
|
177
|
+
const refQ = ref
|
|
178
|
+
? `?ref=${encodeURIComponent(ref)}`
|
|
179
|
+
: config.branch?.trim()
|
|
180
|
+
? `?ref=${encodeURIComponent(config.branch.trim())}`
|
|
181
|
+
: "";
|
|
182
|
+
const pathPart = `/${this.encodePath(filePath)}`;
|
|
183
|
+
|
|
184
|
+
const { status, json } = await this.request<GithubContentResponse>(
|
|
185
|
+
config,
|
|
186
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents${pathPart}${refQ}`,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
this.ensureOk(status, "Failed to read query.");
|
|
190
|
+
|
|
191
|
+
if (!json || Array.isArray(json) || (json as any).type !== "file") {
|
|
192
|
+
throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const file = json as Extract<GithubContentResponse, { type: "file" }>;
|
|
196
|
+
const text = this.base64DecodeUtf8(file.content || "");
|
|
197
|
+
return { text, sha: file.sha };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async readQuery(config: GitWorkspaceConfig, queryId: string): Promise<ReadResult> {
|
|
201
|
+
const { text, sha } = await this.readFileAtRef(config, queryId);
|
|
202
|
+
return { queryText: text, versionTag: sha };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async writeQuery(
|
|
206
|
+
config: GitWorkspaceConfig,
|
|
207
|
+
queryId: string,
|
|
208
|
+
queryText: string,
|
|
209
|
+
options?: WriteQueryOptions,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
212
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
213
|
+
|
|
214
|
+
// If the caller provides an expected version tag, treat it as the current file sha.
|
|
215
|
+
// This avoids an extra GET (and avoids noisy 404s when the file was moved/renamed).
|
|
216
|
+
let currentSha: string | undefined = options?.expectedVersionTag?.trim() || undefined;
|
|
217
|
+
if (!currentSha) {
|
|
218
|
+
// Determine current file sha (if any) so we can update.
|
|
219
|
+
try {
|
|
220
|
+
const res = await this.readFileAtRef(config, queryId);
|
|
221
|
+
currentSha = res.sha;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
const err = e as any;
|
|
224
|
+
if (err?.code !== "NOT_FOUND") throw e;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const message = this.getCommitMessage(queryId, options, !currentSha);
|
|
229
|
+
const body: any = {
|
|
230
|
+
message,
|
|
231
|
+
content: this.base64EncodeUtf8(queryText),
|
|
232
|
+
};
|
|
233
|
+
if (config.branch?.trim()) body.branch = config.branch.trim();
|
|
234
|
+
if (currentSha) body.sha = currentSha;
|
|
235
|
+
|
|
236
|
+
const pathPart = `/${this.encodePath(filePath)}`;
|
|
237
|
+
const { status } = await this.request(
|
|
238
|
+
config,
|
|
239
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents${pathPart}`,
|
|
240
|
+
{
|
|
241
|
+
method: "PUT",
|
|
242
|
+
body: JSON.stringify(body),
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
this.ensureOk(status, "Failed to save query to git workspace.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async listVersions(config: GitWorkspaceConfig, queryId: string): Promise<VersionInfo[]> {
|
|
250
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
251
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
252
|
+
|
|
253
|
+
const qs = new URLSearchParams();
|
|
254
|
+
if (config.branch?.trim()) qs.set("sha", config.branch.trim());
|
|
255
|
+
qs.set("path", filePath);
|
|
256
|
+
qs.set("per_page", "30");
|
|
257
|
+
|
|
258
|
+
const { status, json } = await this.request<GithubCommit[]>(
|
|
259
|
+
config,
|
|
260
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?${qs.toString()}`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
this.ensureOk(status, "Failed to list query versions.");
|
|
264
|
+
|
|
265
|
+
const commits = Array.isArray(json) ? json : [];
|
|
266
|
+
return commits
|
|
267
|
+
.map((c) => {
|
|
268
|
+
const createdAt = c.commit?.committer?.date || c.commit?.author?.date || new Date().toISOString();
|
|
269
|
+
const message = c.commit?.message || "";
|
|
270
|
+
const author = c.author?.login;
|
|
271
|
+
return { id: c.sha, createdAt, author, message } satisfies VersionInfo;
|
|
272
|
+
})
|
|
273
|
+
.filter((v) => !!v.id);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async readVersion(config: GitWorkspaceConfig, queryId: string, versionId: string): Promise<ReadResult> {
|
|
277
|
+
// `versionId` is a commit sha, but writes use the file blob sha for optimistic concurrency.
|
|
278
|
+
// Use the returned file sha so saving still works after opening a historical version.
|
|
279
|
+
const { text, sha } = await this.readFileAtRef(config, queryId, versionId);
|
|
280
|
+
return { queryText: text, versionTag: sha };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void> {
|
|
284
|
+
const { owner, repo } = parseOwnerRepo(config.remoteUrl);
|
|
285
|
+
const filePath = this.joinPath(config.rootPath, queryId);
|
|
286
|
+
|
|
287
|
+
let sha: string | undefined;
|
|
288
|
+
try {
|
|
289
|
+
const res = await this.readFileAtRef(config, queryId);
|
|
290
|
+
sha = res.sha;
|
|
291
|
+
} catch (e) {
|
|
292
|
+
const err = e as any;
|
|
293
|
+
if (err?.code === "NOT_FOUND") return;
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!sha) return;
|
|
298
|
+
|
|
299
|
+
const body: any = {
|
|
300
|
+
message: this.getDeleteMessage(queryId),
|
|
301
|
+
sha,
|
|
302
|
+
};
|
|
303
|
+
if (config.branch?.trim()) body.branch = config.branch.trim();
|
|
304
|
+
|
|
305
|
+
const pathPart = `/${this.encodePath(filePath)}`;
|
|
306
|
+
const { status } = await this.request(
|
|
307
|
+
config,
|
|
308
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents${pathPart}`,
|
|
309
|
+
{
|
|
310
|
+
method: "DELETE",
|
|
311
|
+
body: JSON.stringify(body),
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (status === 404) return;
|
|
317
|
+
this.ensureOk(status, "Failed to delete query.");
|
|
318
|
+
}
|
|
319
|
+
}
|