@matdata/yasqe 5.15.0 → 5.17.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasqe",
3
3
  "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.15.0",
4
+ "version": "5.17.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -105,7 +105,7 @@ describe("Share Functionality", () => {
105
105
  " }",
106
106
  ' ContentType = "application/x-www-form-urlencoded"',
107
107
  ' Body = "query=SELECT"',
108
- ' OutFile = "result.json"',
108
+ ' OutFile = "sparql-generated.json"',
109
109
  "}",
110
110
  "",
111
111
  "Invoke-WebRequest @params",
@@ -117,6 +117,37 @@ describe("Share Functionality", () => {
117
117
  expect(psString).to.include("Headers");
118
118
  expect(psString).to.include("OutFile");
119
119
  expect(psString).to.include("Accept");
120
+ expect(psString).to.include("sparql-generated");
121
+ });
122
+
123
+ it("should format PowerShell commands with here-string for query", () => {
124
+ const query = "SELECT * WHERE { ?s ?p ?o }";
125
+ const lines = [
126
+ '$query = @"',
127
+ query,
128
+ '"@',
129
+ "",
130
+ "$params = @{",
131
+ ' Uri = "https://example.com/sparql"',
132
+ ' Method = "Post"',
133
+ " Headers = @{",
134
+ ' "Accept" = "application/sparql-results+json"',
135
+ " }",
136
+ ' ContentType = "application/x-www-form-urlencoded"',
137
+ ' Body = "query=$([System.Net.WebUtility]::UrlEncode($query))"',
138
+ ' OutFile = "sparql-generated.json"',
139
+ "}",
140
+ "",
141
+ "Invoke-WebRequest @params",
142
+ ];
143
+ const psString = lines.join("\n");
144
+
145
+ expect(psString).to.include('$query = @"');
146
+ expect(psString).to.include('"@');
147
+ expect(psString).to.include(query);
148
+ expect(psString).to.include('Body = "query=$([System.Net.WebUtility]::UrlEncode($query))"');
149
+ expect(psString).to.not.include('Body = "query=`$query"'); // Should NOT escape the variable
150
+ expect(psString).to.include("sparql-generated");
120
151
  });
121
152
 
122
153
  it("should format wget commands with proper line breaks", () => {
package/src/index.ts CHANGED
@@ -46,6 +46,10 @@ export interface Yasqe {
46
46
  off(eventName: "autocompletionClose", handler: (instance: Yasqe) => void): void;
47
47
  on(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
48
48
  off(eventName: "resize", handler: (instance: Yasqe, newSize: string) => void): void;
49
+ on(eventName: "saveManagedQuery", handler: () => void): void;
50
+ off(eventName: "saveManagedQuery", handler: () => void): void;
51
+ on(eventName: "downloadRqFile", handler: () => void): void;
52
+ off(eventName: "downloadRqFile", handler: () => void): void;
49
53
  on(eventName: string, handler: () => void): void;
50
54
  }
51
55
 
@@ -59,7 +63,7 @@ export class Yasqe extends CodeMirror {
59
63
  private abortController: AbortController | undefined;
60
64
  private queryStatus: "valid" | "error" | undefined;
61
65
  private queryBtn: HTMLButtonElement | undefined;
62
- private saveBtn: HTMLButtonElement | undefined;
66
+ private saveBtnWrapper: HTMLDivElement | undefined;
63
67
  private fullscreenBtn: HTMLButtonElement | undefined;
64
68
  private hamburgerBtn: HTMLButtonElement | undefined;
65
69
  private hamburgerMenu: HTMLDivElement | undefined;
@@ -570,24 +574,42 @@ export class Yasqe extends CodeMirror {
570
574
  }
571
575
 
572
576
  /**
573
- * Draw save button (THIRD)
577
+ * Draw save buttons (THIRD)
574
578
  */
575
- const saveBtn = document.createElement("button");
576
- addClass(saveBtn, "yasqe_saveButton");
577
- const saveIcon = document.createElement("i");
578
- addClass(saveIcon, "fas");
579
- addClass(saveIcon, "fa-save");
580
- saveIcon.setAttribute("aria-hidden", "true");
581
- saveBtn.appendChild(saveIcon);
582
- saveBtn.onclick = () => {
583
- // Call the managed query save function if available
579
+ const saveBtnWrapper = document.createElement("div");
580
+ addClass(saveBtnWrapper, "yasqe_saveWrapper");
581
+ saveBtnWrapper.style.display = "none"; // Hidden by default, shown when workspace is configured
582
+ this.saveBtnWrapper = saveBtnWrapper;
583
+
584
+ const saveManagedBtn = document.createElement("button");
585
+ addClass(saveManagedBtn, "yasqe_saveManagedButton");
586
+ const saveManagedIcon = document.createElement("i");
587
+ addClass(saveManagedIcon, "fas");
588
+ addClass(saveManagedIcon, "fa-database");
589
+ saveManagedIcon.setAttribute("aria-hidden", "true");
590
+ saveManagedBtn.appendChild(saveManagedIcon);
591
+ saveManagedBtn.title = "Save as managed query";
592
+ saveManagedBtn.setAttribute("aria-label", "Save as managed query");
593
+ saveManagedBtn.onclick = () => {
584
594
  this.emit("saveManagedQuery");
585
595
  };
586
- saveBtn.title = "Save managed query (Ctrl+S)";
587
- saveBtn.setAttribute("aria-label", "Save managed query");
588
- saveBtn.style.display = "none"; // Hidden by default, shown when workspace is configured
589
- this.saveBtn = saveBtn;
590
- buttons.appendChild(saveBtn);
596
+ saveBtnWrapper.appendChild(saveManagedBtn);
597
+
598
+ const saveRqBtn = document.createElement("button");
599
+ addClass(saveRqBtn, "yasqe_saveRqButton");
600
+ const saveRqIcon = document.createElement("i");
601
+ addClass(saveRqIcon, "fas");
602
+ addClass(saveRqIcon, "fa-file-download");
603
+ saveRqIcon.setAttribute("aria-hidden", "true");
604
+ saveRqBtn.appendChild(saveRqIcon);
605
+ saveRqBtn.title = "Save as .rq file";
606
+ saveRqBtn.setAttribute("aria-label", "Save as .rq file");
607
+ saveRqBtn.onclick = () => {
608
+ this.emit("downloadRqFile");
609
+ };
610
+ saveBtnWrapper.appendChild(saveRqBtn);
611
+
612
+ buttons.appendChild(saveBtnWrapper);
591
613
 
592
614
  /**
593
615
  * Draw format btn (FOURTH)
@@ -671,21 +693,37 @@ export class Yasqe extends CodeMirror {
671
693
  this.hamburgerMenu.appendChild(shareItem);
672
694
  }
673
695
 
674
- const saveItem = document.createElement("button");
675
- saveItem.className = "yasqe_hamburgerMenuItem";
676
- const saveIconMenu = document.createElement("i");
677
- addClass(saveIconMenu, "fas");
678
- addClass(saveIconMenu, "fa-save");
679
- saveIconMenu.setAttribute("aria-hidden", "true");
680
- saveItem.appendChild(saveIconMenu);
681
- const saveLabel = document.createElement("span");
682
- saveLabel.textContent = "Save";
683
- saveItem.appendChild(saveLabel);
684
- saveItem.onclick = () => {
696
+ const saveManagedItem = document.createElement("button");
697
+ saveManagedItem.className = "yasqe_hamburgerMenuItem";
698
+ const saveManagedIconMenu = document.createElement("i");
699
+ addClass(saveManagedIconMenu, "fas");
700
+ addClass(saveManagedIconMenu, "fa-database");
701
+ saveManagedIconMenu.setAttribute("aria-hidden", "true");
702
+ saveManagedItem.appendChild(saveManagedIconMenu);
703
+ const saveManagedLabel = document.createElement("span");
704
+ saveManagedLabel.textContent = "Save as managed query";
705
+ saveManagedItem.appendChild(saveManagedLabel);
706
+ saveManagedItem.onclick = () => {
685
707
  this.closeHamburgerMenu();
686
708
  this.emit("saveManagedQuery");
687
709
  };
688
- this.hamburgerMenu.appendChild(saveItem);
710
+ this.hamburgerMenu.appendChild(saveManagedItem);
711
+
712
+ const saveRqItem = document.createElement("button");
713
+ saveRqItem.className = "yasqe_hamburgerMenuItem";
714
+ const saveRqIconMenu = document.createElement("i");
715
+ addClass(saveRqIconMenu, "fas");
716
+ addClass(saveRqIconMenu, "fa-file-download");
717
+ saveRqIconMenu.setAttribute("aria-hidden", "true");
718
+ saveRqItem.appendChild(saveRqIconMenu);
719
+ const saveRqLabel = document.createElement("span");
720
+ saveRqLabel.textContent = "Save as .rq file";
721
+ saveRqItem.appendChild(saveRqLabel);
722
+ saveRqItem.onclick = () => {
723
+ this.closeHamburgerMenu();
724
+ this.emit("downloadRqFile");
725
+ };
726
+ this.hamburgerMenu.appendChild(saveRqItem);
689
727
 
690
728
  if (this.config.showFormatButton) {
691
729
  const formatItem = document.createElement("button");
@@ -1553,8 +1591,8 @@ export class Yasqe extends CodeMirror {
1553
1591
  }
1554
1592
 
1555
1593
  public setSaveButtonVisible(visible: boolean) {
1556
- if (this.saveBtn) {
1557
- this.saveBtn.style.display = visible ? "inline-flex" : "none";
1594
+ if (this.saveBtnWrapper) {
1595
+ this.saveBtnWrapper.style.display = visible ? "inline-flex" : "none";
1558
1596
  }
1559
1597
  }
1560
1598
 
@@ -293,25 +293,33 @@
293
293
  }
294
294
  }
295
295
 
296
- .yasqe_saveButton {
296
+ .yasqe_saveWrapper {
297
+ position: relative;
297
298
  display: inline-flex;
298
299
  align-items: center;
299
- justify-content: center;
300
- border: none;
301
- background: none;
302
- cursor: pointer;
303
- padding: 6px;
300
+ vertical-align: middle;
304
301
  margin-left: 8px;
305
- height: 36px;
306
- width: 36px;
307
- color: var(--yasgui-text-secondary, #505050);
308
302
 
309
- i {
310
- font-size: 20px;
311
- }
303
+ .yasqe_saveManagedButton,
304
+ .yasqe_saveRqButton {
305
+ display: inline-flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ border: none;
309
+ background: none;
310
+ cursor: pointer;
311
+ padding: 6px;
312
+ height: 36px;
313
+ width: 36px;
314
+ color: var(--yasgui-text-secondary, #505050);
312
315
 
313
- &:hover {
314
- color: #337ab7;
316
+ i {
317
+ font-size: 20px;
318
+ }
319
+
320
+ &:hover {
321
+ color: #337ab7;
322
+ }
315
323
  }
316
324
  }
317
325
 
@@ -441,7 +449,7 @@
441
449
  @media (max-width: 768px) {
442
450
  .yasqe_buttons {
443
451
  .yasqe_share,
444
- .yasqe_saveButton,
452
+ .yasqe_saveWrapper,
445
453
  .yasqe_formatButton,
446
454
  .yasqe_fullscreenButton {
447
455
  display: none !important;
package/src/sparql.ts CHANGED
@@ -174,6 +174,13 @@ export function getAjaxConfig(
174
174
  export interface ExecuteQueryOptions {
175
175
  customQuery?: string;
176
176
  customAccept?: string;
177
+ /** Optional external abort signal, useful for plugin-driven background fetches. */
178
+ signal?: AbortSignal;
179
+ /**
180
+ * Execute without emitting Yasqe query lifecycle events.
181
+ * Useful for background/plugin-driven queries that should not update main UI state.
182
+ */
183
+ silent?: boolean;
177
184
  }
178
185
 
179
186
  export async function executeQuery(
@@ -182,13 +189,21 @@ export async function executeQuery(
182
189
  options?: ExecuteQueryOptions,
183
190
  ): Promise<any> {
184
191
  const queryStart = Date.now();
192
+ const silent = !!options?.silent;
185
193
  try {
186
- yasqe.emit("queryBefore", yasqe, config);
194
+ if (!silent) yasqe.emit("queryBefore", yasqe, config);
187
195
  const populatedConfig = getAjaxConfig(yasqe, config);
188
196
  if (!populatedConfig) {
189
197
  return; // Nothing to query
190
198
  }
191
199
  const abortController = new AbortController();
200
+ if (options?.signal) {
201
+ if (options.signal.aborted) {
202
+ abortController.abort();
203
+ } else {
204
+ options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
205
+ }
206
+ }
192
207
 
193
208
  // Use custom accept header if provided, otherwise use the default
194
209
  const acceptHeader = options?.customAccept || populatedConfig.accept;
@@ -256,17 +271,22 @@ export async function executeQuery(
256
271
  populatedConfig.url = url.toString();
257
272
  }
258
273
  const request = new Request(populatedConfig.url, fetchOptions);
259
- yasqe.emit("query", request, abortController);
274
+ if (!silent) yasqe.emit("query", request, abortController);
260
275
  const response = await fetch(request);
261
276
 
262
277
  // Await the response content and merge with the `Response` object
278
+ const content = await response.text();
263
279
  const queryResponse = {
264
280
  ok: response.ok,
265
281
  status: response.status,
266
282
  statusText: response.statusText,
267
283
  headers: response.headers,
268
284
  type: response.type,
269
- content: await response.text(),
285
+ content,
286
+ // Compatibility aliases for plugins that expect fetch-like or axios-like response objects.
287
+ data: content,
288
+ json: async () => JSON.parse(content),
289
+ text: async () => content,
270
290
  };
271
291
 
272
292
  if (!response.ok) {
@@ -278,8 +298,10 @@ export async function executeQuery(
278
298
  throw error;
279
299
  }
280
300
 
281
- yasqe.emit("queryResponse", queryResponse, Date.now() - queryStart);
282
- yasqe.emit("queryResults", queryResponse.content, Date.now() - queryStart);
301
+ if (!silent) {
302
+ yasqe.emit("queryResponse", queryResponse, Date.now() - queryStart);
303
+ yasqe.emit("queryResults", queryResponse.content, Date.now() - queryStart);
304
+ }
283
305
  return queryResponse;
284
306
  } catch (e) {
285
307
  if (e instanceof Error && e.message === "Aborted") {
@@ -292,12 +314,12 @@ export async function executeQuery(
292
314
  if (e.message.includes("Failed to fetch") || e.message.includes("NetworkError")) {
293
315
  enhancedError.message = `${e.message}. The server may have returned an error response (check browser dev tools), but CORS headers are preventing JavaScript from accessing it. Ensure the endpoint returns proper CORS headers even for error responses (Access-Control-Allow-Origin, etc.).`;
294
316
  }
295
- yasqe.emit("queryResponse", enhancedError, Date.now() - queryStart);
317
+ if (!silent) yasqe.emit("queryResponse", enhancedError, Date.now() - queryStart);
296
318
  } else {
297
- yasqe.emit("queryResponse", e, Date.now() - queryStart);
319
+ if (!silent) yasqe.emit("queryResponse", e, Date.now() - queryStart);
298
320
  }
299
321
  }
300
- yasqe.emit("error", e);
322
+ if (!silent) yasqe.emit("error", e);
301
323
  throw e;
302
324
  }
303
325
  }
@@ -520,10 +542,48 @@ export function getAsPowerShellString(yasqe: Yasqe, _config?: Config["requestCon
520
542
  lines.push(headersLines.join("\n"));
521
543
  lines.push(" }");
522
544
  }
523
- lines.push(` OutFile = "result.${fileExtension}"`);
545
+ lines.push(` OutFile = "sparql-generated.${fileExtension}"`);
524
546
  lines.push("}");
525
547
  } else if (ajaxConfig.reqMethod === "POST") {
526
- const body = queryString.stringify(ajaxConfig.args);
548
+ // Extract the query/update parameter and other parameters separately
549
+ // Determine the query parameter name first (query takes precedence over update)
550
+ const queryParamName = ajaxConfig.args.query !== undefined ? "query" : "update";
551
+ const queryParam = ajaxConfig.args[queryParamName];
552
+
553
+ const otherArgs: RequestArgs = {};
554
+ for (const key in ajaxConfig.args) {
555
+ if (key !== "query" && key !== "update") {
556
+ otherArgs[key] = ajaxConfig.args[key];
557
+ }
558
+ }
559
+
560
+ // Build the query string using here-string for easy editing
561
+ if (queryParam) {
562
+ // Handle both string and string[] cases - use first element if array
563
+ const queryText = Array.isArray(queryParam) ? queryParam[0] : queryParam;
564
+ lines.push(`$${queryParamName} = @"`);
565
+ lines.push(queryText);
566
+ lines.push(`"@`);
567
+ lines.push("");
568
+ }
569
+
570
+ // Build the body with the query variable and any other parameters
571
+ // The query must be URL-encoded for application/x-www-form-urlencoded
572
+ let bodyExpression: string;
573
+ const urlEncodeExpr = `[System.Net.WebUtility]::UrlEncode($${queryParamName})`;
574
+ if (queryParam && Object.keys(otherArgs).length > 0) {
575
+ // Both query variable and other args
576
+ const otherArgsString = queryString.stringify(otherArgs);
577
+ bodyExpression = `"${queryParamName}=$(${urlEncodeExpr})&${escapePowerShellString(otherArgsString)}"`;
578
+ } else if (queryParam) {
579
+ // Only query variable - use subexpression for URL encoding
580
+ bodyExpression = `"${queryParamName}=$(${urlEncodeExpr})"`;
581
+ } else {
582
+ // Only other args (shouldn't happen, but handle it)
583
+ const otherArgsString = queryString.stringify(otherArgs);
584
+ bodyExpression = `"${escapePowerShellString(otherArgsString)}"`;
585
+ }
586
+
527
587
  lines.push("$params = @{");
528
588
  lines.push(` Uri = "${escapePowerShellString(url)}"`);
529
589
  lines.push(` Method = "Post"`);
@@ -533,8 +593,8 @@ export function getAsPowerShellString(yasqe: Yasqe, _config?: Config["requestCon
533
593
  lines.push(" }");
534
594
  }
535
595
  lines.push(` ContentType = "application/x-www-form-urlencoded"`);
536
- lines.push(` Body = "${escapePowerShellString(body)}"`);
537
- lines.push(` OutFile = "result.${fileExtension}"`);
596
+ lines.push(` Body = ${bodyExpression}`);
597
+ lines.push(` OutFile = "sparql-generated.${fileExtension}"`);
538
598
  lines.push("}");
539
599
  } else {
540
600
  // Handle other methods (PUT, DELETE, etc.)
@@ -552,7 +612,7 @@ export function getAsPowerShellString(yasqe: Yasqe, _config?: Config["requestCon
552
612
  lines.push(` ContentType = "application/x-www-form-urlencoded"`);
553
613
  lines.push(` Body = "${body.replace(/"/g, '`"')}"`);
554
614
  }
555
- lines.push(` OutFile = "result.${fileExtension}"`);
615
+ lines.push(` OutFile = "sparql-generated.${fileExtension}"`);
556
616
  lines.push("}");
557
617
  }
558
618