@bestcodetools/graphql-playground 0.0.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.
@@ -0,0 +1,1016 @@
1
+ var app = angular.module('app', []);
2
+
3
+ app.factory('AppState', function () {
4
+ const tryParse = (value) => {
5
+ if(value === null) return null;
6
+ try { return JSON.parse(value); }
7
+ catch (e) { return value; }
8
+ };
9
+ return {
10
+ saveKey: function (key, value) {
11
+ sessionStorage.setItem(key, angular.$$stringify(value));
12
+ },
13
+ getKey: function (key) {
14
+ const value = sessionStorage.getItem(key);
15
+ return tryParse(value);
16
+ }
17
+ };
18
+ });
19
+
20
+ app.factory('I18nService', ['AppState', function (AppState) {
21
+ const STORAGE_KEY = 'locale';
22
+ const dictionaries = {
23
+ en: {
24
+ 'app.name': 'GraphQL Playground',
25
+ 'app.developed_by': 'Developed by BestCodeTools',
26
+ 'nav.settings': 'Settings',
27
+ 'nav.language': 'Language',
28
+ 'language.en': 'English',
29
+ 'language.pt-BR': 'Portuguese (Brazil)',
30
+ 'actions.send': 'Send',
31
+ 'actions.close': 'Close',
32
+ 'actions.format': 'Format',
33
+ 'actions.new_query': 'New Query',
34
+ 'actions.new_mutation': 'New Mutation',
35
+ 'modal.settings': 'Settings',
36
+ 'tabs.default': 'Tab',
37
+ 'editor.query': 'Query Editor',
38
+ 'editor.response': 'Response Body',
39
+ 'editor.variables': 'Variables Payload',
40
+ 'editor.headers': 'Request Headers',
41
+ 'schema.title': 'Schema Viewer',
42
+ 'schema.search_placeholder': 'Search types or first-level fields',
43
+ 'schema.note': 'Ctrl+Click a root query, mutation, or subscription field to add it to the active payload with operation variables.',
44
+ 'schema.error': 'An error occurred while loading the schema',
45
+ 'schema.retry': 'Retry to load schema',
46
+ 'config.shared_headers': 'Shared Headers',
47
+ 'config.other': 'Other',
48
+ 'config.property': 'Property',
49
+ 'config.value': 'Value',
50
+ 'config.key_placeholder': 'Key',
51
+ 'config.value_placeholder': 'Value',
52
+ 'config.add_header': '+ Add Header',
53
+ 'config.other_placeholder': 'Other',
54
+ 'workspace.export': 'Export Workspace',
55
+ 'workspace.import': 'Import Workspace',
56
+ 'workspace.note': 'Export and import your full workspace, including tabs and settings.',
57
+ 'workspace.import_error': 'Could not import this workspace file.',
58
+ 'workspace.token_placeholder': '[your token here]',
59
+ 'workspace.key_placeholder': '[your key here]',
60
+ 'query.placeholder': '/* Type your query here */'
61
+ },
62
+ 'pt-BR': {
63
+ 'app.name': 'GraphQL Playground',
64
+ 'app.developed_by': 'Desenvolvido por BestCodeTools',
65
+ 'nav.settings': 'Configurações',
66
+ 'nav.language': 'Idioma',
67
+ 'language.en': 'English',
68
+ 'language.pt-BR': 'Português (Brasil)',
69
+ 'actions.send': 'Enviar',
70
+ 'actions.close': 'Fechar',
71
+ 'actions.format': 'Formatar',
72
+ 'actions.new_query': 'Nova Query',
73
+ 'actions.new_mutation': 'Nova Mutation',
74
+ 'modal.settings': 'Configurações',
75
+ 'tabs.default': 'Aba',
76
+ 'editor.query': 'Editor de Query',
77
+ 'editor.response': 'Response Body',
78
+ 'editor.variables': 'Payload de Variables',
79
+ 'editor.headers': 'Request Headers',
80
+ 'schema.title': 'Schema Viewer',
81
+ 'schema.search_placeholder': 'Buscar types ou fields de primeiro nível',
82
+ 'schema.note': 'Ctrl+Click em um field raiz de query, mutation ou subscription para adicionar ao payload ativo com operation variables.',
83
+ 'schema.error': 'Ocorreu um erro ao carregar o schema',
84
+ 'schema.retry': 'Tentar carregar o schema novamente',
85
+ 'config.shared_headers': 'Shared Headers',
86
+ 'config.other': 'Outros',
87
+ 'config.property': 'Propriedade',
88
+ 'config.value': 'Valor',
89
+ 'config.key_placeholder': 'Chave',
90
+ 'config.value_placeholder': 'Valor',
91
+ 'config.add_header': '+ Adicionar Header',
92
+ 'config.other_placeholder': 'Outras Configurações (placeholder)',
93
+ 'query.placeholder': '/* Digite sua query aqui */'
94
+ }
95
+ };
96
+
97
+ let locale = AppState.getKey(STORAGE_KEY) || 'pt-BR';
98
+ if (!dictionaries[locale]) {
99
+ locale = 'pt-BR';
100
+ }
101
+
102
+ return {
103
+ getLocale: function () {
104
+ return locale;
105
+ },
106
+ setLocale: function (nextLocale) {
107
+ if (!dictionaries[nextLocale]) {
108
+ return locale;
109
+ }
110
+
111
+ locale = nextLocale;
112
+ AppState.saveKey(STORAGE_KEY, locale);
113
+ return locale;
114
+ },
115
+ getLocales: function () {
116
+ return [
117
+ { code: 'en', labelKey: 'language.en' },
118
+ { code: 'pt-BR', labelKey: 'language.pt-BR' }
119
+ ];
120
+ },
121
+ t: function (key) {
122
+ return (dictionaries[locale] && dictionaries[locale][key])
123
+ || (dictionaries.en && dictionaries.en[key])
124
+ || key;
125
+ }
126
+ };
127
+ }]);
128
+ // Serviço para gerenciar o estado das abas
129
+ app.factory('TabService', ['AppState', function (state) {
130
+ const STORAGE_KEY = 'tabs';
131
+
132
+ return {
133
+ getTabs: function () {
134
+ const tabs = state.getKey(STORAGE_KEY);
135
+ return tabs ? tabs : [];
136
+ },
137
+ saveTabs: function (tabs) {
138
+ state.saveKey(STORAGE_KEY, tabs);
139
+ },
140
+ generateTabId: function () {
141
+ return Date.now() + Math.random().toString(36).substr(2, 5);
142
+ }
143
+ };
144
+ }]);
145
+
146
+ app.controller('MainController', ['$scope', '$timeout', 'AppState', 'TabService', 'I18nService', function ($scope, $timeout, AppState, TabService, I18nService) {
147
+ const $ctrl = this;
148
+ const ACTIVE_TAB_STORAGE_KEY = 'activeTab';
149
+ const SHARED_HEADERS_STORAGE_KEY = 'sharedHeaders';
150
+ $scope.$ctrl = $ctrl;
151
+ // Estado inicial
152
+ $ctrl.title = 'AngularJS Tutorial Example';
153
+ $ctrl.message = 'Hello World!';
154
+ $ctrl.ready = true;
155
+ $ctrl.appVersion = 'v1.0.2';
156
+ $ctrl.locales = I18nService.getLocales();
157
+ $ctrl.locale = I18nService.getLocale();
158
+ $ctrl.t = function (key) {
159
+ return I18nService.t(key);
160
+ };
161
+ $ctrl.tabs = [];
162
+ $ctrl.activeTab = 0;
163
+ $ctrl.schema = null;
164
+ $ctrl.loadSchemaError = null;
165
+ function getDefaultGraphqlUrl() {
166
+ if (typeof window === 'undefined' || !window.location) {
167
+ return 'http://localhost:4000/graphql';
168
+ }
169
+
170
+ const { protocol, hostname, host } = window.location;
171
+ const normalizedHost = String(hostname || '').toLowerCase();
172
+ const isLoopbackHost = normalizedHost === 'localhost'
173
+ || normalizedHost === '127.0.0.1'
174
+ || normalizedHost === '::1'
175
+ || normalizedHost === '[::1]';
176
+
177
+ if (isLoopbackHost) {
178
+ return 'http://localhost:4000/graphql';
179
+ }
180
+
181
+ return `${protocol}//${host}/graphql`;
182
+ }
183
+
184
+ $ctrl.url = sessionStorage.getItem('url') || getDefaultGraphqlUrl();
185
+ // Estado inicial
186
+ $ctrl.showConfig = false;
187
+
188
+ // Abrir e fechar modal
189
+ $ctrl.openConfig = function () {
190
+ $ctrl.showConfig = true;
191
+ };
192
+
193
+ $ctrl.closeConfig = function () {
194
+ $ctrl.showConfig = false;
195
+ };
196
+ $ctrl.setLocale = function (locale) {
197
+ $ctrl.locale = I18nService.setLocale(locale);
198
+ };
199
+ function debounce(func, wait) {
200
+ let timeout;
201
+ return function (...args) {
202
+ clearTimeout(timeout);
203
+ timeout = setTimeout(() => func.apply(this, args), wait);
204
+ };
205
+ }
206
+ const persistTabsDebounced = debounce(() => {
207
+ TabService.saveTabs($ctrl.tabs);
208
+ }, 250);
209
+ $ctrl.persistTabs = function () {
210
+ persistTabsDebounced();
211
+ sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
212
+ };
213
+ $ctrl.saveTabsToSessionStorage = function () {
214
+ $ctrl.persistTabs();
215
+ };
216
+
217
+ function getTabTitleFromQuery(queryText, tabIndex) {
218
+ const safeQuery = typeof queryText === 'string' ? queryText : '';
219
+ const queryWithoutComments = safeQuery
220
+ .replace(/\/\*[\s\S]*?\*\//g, '')
221
+ .replace(/([^:]|^)\/\/.*$/gm, '$1')
222
+ .replace(/#.*/g, '');
223
+
224
+ const namedOperationMatch = /\b(query|mutation|subscription)\s+([^\(\{\}\)\s]+)/.exec(queryWithoutComments);
225
+ if (namedOperationMatch) {
226
+ return `${namedOperationMatch[1]} ${namedOperationMatch[2]}`;
227
+ }
228
+
229
+ const anonymousOperationMatch = /\b(query|mutation|subscription)\b\s*(\([^)]*\))?\s*\{([\s\S]*)\}/.exec(queryWithoutComments);
230
+ if (anonymousOperationMatch) {
231
+ const selectionBody = anonymousOperationMatch[3] || '';
232
+ const firstRootFieldMatch = /(^|[\s{])([A-Za-z_][A-Za-z0-9_]*)(?=\s*(\(|\{|@|$|\n))/m.exec(selectionBody);
233
+
234
+ if (firstRootFieldMatch && firstRootFieldMatch[2]) {
235
+ return firstRootFieldMatch[2];
236
+ }
237
+ }
238
+
239
+ return `${$ctrl.t('tabs.default')} ${tabIndex}`;
240
+ }
241
+
242
+ function getWorkspaceLabel(key) {
243
+ const isPtBr = I18nService.getLocale() === 'pt-BR';
244
+
245
+ const labels = {
246
+ export: isPtBr ? 'Exportar Workspace' : 'Export Workspace',
247
+ import: isPtBr ? 'Importar Workspace' : 'Import Workspace',
248
+ note: isPtBr
249
+ ? 'Exporte e importe seu workspace completo, incluindo abas e configurações.'
250
+ : 'Export and import your full workspace, including tabs and settings.',
251
+ importError: isPtBr
252
+ ? 'Não foi possível importar este arquivo de workspace.'
253
+ : 'Could not import this workspace file.',
254
+ tokenPlaceholder: isPtBr ? '[seu token aqui]' : '[your token here]',
255
+ keyPlaceholder: isPtBr ? '[sua chave aqui]' : '[your key here]'
256
+ };
257
+
258
+ return labels[key] || '';
259
+ }
260
+
261
+ function isSensitiveHeaderName(headerName) {
262
+ const normalizedName = String(headerName || '').trim().toLowerCase();
263
+ return normalizedName === 'authorization' || normalizedName.includes('token') || normalizedName.includes('key');
264
+ }
265
+
266
+ function getHeaderExportPlaceholder(headerName) {
267
+ const normalizedName = String(headerName || '').trim().toLowerCase();
268
+ return normalizedName.includes('key')
269
+ ? getWorkspaceLabel('keyPlaceholder')
270
+ : getWorkspaceLabel('tokenPlaceholder');
271
+ }
272
+
273
+ function sanitizeHeadersTextForExport(headersText) {
274
+ const safeText = typeof headersText === 'string' ? headersText : '{}';
275
+
276
+ try {
277
+ const parsed = JSON.parse(safeText);
278
+
279
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
280
+ return safeText;
281
+ }
282
+
283
+ const sanitized = Object.keys(parsed).reduce((acc, key) => {
284
+ acc[key] = isSensitiveHeaderName(key)
285
+ ? getHeaderExportPlaceholder(key)
286
+ : parsed[key];
287
+ return acc;
288
+ }, {});
289
+
290
+ return JSON.stringify(sanitized, null, 2);
291
+ } catch (error) {
292
+ return safeText.replace(/"([^"]+)"\s*:\s*"([^"]*)"/g, (match, key) => {
293
+ if (!isSensitiveHeaderName(key)) {
294
+ return match;
295
+ }
296
+
297
+ return `"${key}": "${getHeaderExportPlaceholder(key)}"`;
298
+ });
299
+ }
300
+ }
301
+
302
+ function sanitizeSharedHeadersForExport(headers) {
303
+ if (!Array.isArray(headers)) {
304
+ return [];
305
+ }
306
+
307
+ return headers.map((header) => {
308
+ const key = header && typeof header.key === 'string' ? header.key : '';
309
+ const value = header && typeof header.value === 'string' ? header.value : '';
310
+
311
+ return {
312
+ key,
313
+ value: isSensitiveHeaderName(key) ? getHeaderExportPlaceholder(key) : value
314
+ };
315
+ });
316
+ }
317
+
318
+ function parseJsonObject(text, fallbackValue) {
319
+ const safeText = typeof text === 'string' ? text.trim() : '';
320
+
321
+ if (!safeText) {
322
+ return fallbackValue;
323
+ }
324
+
325
+ const parsed = JSON.parse(safeText);
326
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
327
+ throw new Error('Expected a JSON object.');
328
+ }
329
+
330
+ return parsed;
331
+ }
332
+
333
+ function getSharedHeadersMap() {
334
+ const sharedHeaders = AppState.getKey(SHARED_HEADERS_STORAGE_KEY) || [];
335
+
336
+ if (!Array.isArray(sharedHeaders)) {
337
+ return {};
338
+ }
339
+
340
+ return sharedHeaders.reduce((acc, header) => {
341
+ const key = header && typeof header.key === 'string' ? header.key.trim() : '';
342
+ const value = header && typeof header.value === 'string' ? header.value : '';
343
+
344
+ if (!key) {
345
+ return acc;
346
+ }
347
+
348
+ acc[key] = value;
349
+ return acc;
350
+ }, {});
351
+ }
352
+
353
+ function formatResponseBody(response, payload) {
354
+ const contentType = response.headers.get('content-type') || '';
355
+ const isJsonResponse = contentType.toLowerCase().includes('application/json');
356
+
357
+ return Promise.resolve().then(() => {
358
+ if (isJsonResponse) {
359
+ return response.json().then((data) => JSON.stringify(data, null, 2));
360
+ }
361
+
362
+ return response.text().then((text) => {
363
+ if (!text) {
364
+ return JSON.stringify(payload, null, 2);
365
+ }
366
+
367
+ try {
368
+ return JSON.stringify(JSON.parse(text), null, 2);
369
+ } catch (error) {
370
+ return text;
371
+ }
372
+ });
373
+ });
374
+ }
375
+
376
+ function applyAutomaticTabTitle(tab, tabIndex) {
377
+ if (!tab) {
378
+ return;
379
+ }
380
+
381
+ const manualTitle = typeof tab.userDefinedLabel === 'string' ? tab.userDefinedLabel.trim() : '';
382
+ tab.title = manualTitle || getTabTitleFromQuery(tab.query, tabIndex);
383
+ }
384
+
385
+ function normalizeTab(tab, index) {
386
+ const normalizedTab = {
387
+ id: tab && tab.id ? tab.id : TabService.generateTabId(),
388
+ title: tab && typeof tab.title === 'string' ? tab.title : `${$ctrl.t('tabs.default')} ${index}`,
389
+ userDefinedLabel: tab && typeof tab.userDefinedLabel === 'string'
390
+ ? tab.userDefinedLabel
391
+ : (tab && tab.hasManualTitle ? (tab.title || '') : ''),
392
+ query: tab && typeof tab.query === 'string' ? tab.query : $ctrl.t('query.placeholder'),
393
+ variables: tab && typeof tab.variables === 'string' ? tab.variables : '{}',
394
+ headers: tab && typeof tab.headers === 'string' ? tab.headers : '{}',
395
+ result: tab && typeof tab.result === 'string' ? tab.result : '',
396
+ isEditingTitle: false
397
+ };
398
+
399
+ applyAutomaticTabTitle(normalizedTab, index);
400
+ delete normalizedTab.hasManualTitle;
401
+ return normalizedTab;
402
+ }
403
+
404
+ function setTabs(nextTabs) {
405
+ const normalizedTabs = Array.isArray(nextTabs) && nextTabs.length
406
+ ? nextTabs.map((tab, index) => normalizeTab(tab, index))
407
+ : [normalizeTab({}, 0)];
408
+
409
+ $ctrl.tabs = normalizedTabs;
410
+ TabService.saveTabs($ctrl.tabs);
411
+ }
412
+
413
+ function refreshActiveTabEditors() {
414
+ const activeTab = $ctrl.tabs[$ctrl.activeTab];
415
+
416
+ if (!activeTab) {
417
+ return;
418
+ }
419
+
420
+ $timeout(() => {
421
+ if (activeTab.queryEditorApi && activeTab.queryEditorApi.refresh) {
422
+ activeTab.queryEditorApi.refresh();
423
+ }
424
+
425
+ if (activeTab.variablesEditorApi && activeTab.variablesEditorApi.refresh) {
426
+ activeTab.variablesEditorApi.refresh();
427
+ }
428
+
429
+ if (activeTab.headersEditorApi && activeTab.headersEditorApi.refresh) {
430
+ activeTab.headersEditorApi.refresh();
431
+ }
432
+
433
+ if (activeTab.responseViewerApi && activeTab.responseViewerApi.refresh) {
434
+ activeTab.responseViewerApi.refresh();
435
+ }
436
+ }, 0);
437
+
438
+ $timeout(() => {
439
+ if (activeTab.queryEditorApi && activeTab.queryEditorApi.refresh) {
440
+ activeTab.queryEditorApi.refresh();
441
+ }
442
+
443
+ if (activeTab.variablesEditorApi && activeTab.variablesEditorApi.refresh) {
444
+ activeTab.variablesEditorApi.refresh();
445
+ }
446
+
447
+ if (activeTab.headersEditorApi && activeTab.headersEditorApi.refresh) {
448
+ activeTab.headersEditorApi.refresh();
449
+ }
450
+
451
+ if (activeTab.responseViewerApi && activeTab.responseViewerApi.refresh) {
452
+ activeTab.responseViewerApi.refresh();
453
+ }
454
+ }, 120);
455
+ }
456
+
457
+ $ctrl.beginTabTitleEdit = function (tabIndex, $event) {
458
+ const tab = $ctrl.tabs[tabIndex];
459
+
460
+ if (!tab) {
461
+ return;
462
+ }
463
+
464
+ if ($event) {
465
+ $event.preventDefault();
466
+ $event.stopPropagation();
467
+ }
468
+
469
+ $ctrl.activeTab = tabIndex;
470
+ tab.isEditingTitle = true;
471
+ tab.titleDraft = (typeof tab.userDefinedLabel === 'string' && tab.userDefinedLabel.trim())
472
+ ? tab.userDefinedLabel
473
+ : (tab.title || '');
474
+
475
+ $timeout(() => {
476
+ const input = document.getElementById(`tab-title-editor-${tab.id}`);
477
+ if (!input) {
478
+ return;
479
+ }
480
+
481
+ input.focus();
482
+ input.select();
483
+ }, 0);
484
+ };
485
+
486
+ $ctrl.commitTabTitleEdit = function (tabIndex) {
487
+ const tab = $ctrl.tabs[tabIndex];
488
+
489
+ if (!tab) {
490
+ return;
491
+ }
492
+
493
+ const nextTitle = (tab.titleDraft || '').trim();
494
+ tab.isEditingTitle = false;
495
+
496
+ tab.userDefinedLabel = nextTitle || '';
497
+ applyAutomaticTabTitle(tab, tabIndex);
498
+
499
+ delete tab.titleDraft;
500
+ $ctrl.persistTabs();
501
+ };
502
+
503
+ $ctrl.cancelTabTitleEdit = function (tabIndex) {
504
+ const tab = $ctrl.tabs[tabIndex];
505
+
506
+ if (!tab) {
507
+ return;
508
+ }
509
+
510
+ tab.isEditingTitle = false;
511
+ delete tab.titleDraft;
512
+ };
513
+
514
+ $ctrl.handleTabTitleKeydown = function ($event, tabIndex) {
515
+ if ($event.key === 'Enter') {
516
+ $event.preventDefault();
517
+ $ctrl.commitTabTitleEdit(tabIndex);
518
+ return;
519
+ }
520
+
521
+ if ($event.key === 'Escape') {
522
+ $event.preventDefault();
523
+ $ctrl.cancelTabTitleEdit(tabIndex);
524
+ }
525
+ };
526
+
527
+ $ctrl.handleTabMouseDown = function ($event, tabIndex) {
528
+ if (!$event || $event.button !== 1 || $ctrl.tabs.length <= 1) {
529
+ return;
530
+ }
531
+
532
+ $event.preventDefault();
533
+ $event.stopPropagation();
534
+ $ctrl.closeTab(tabIndex);
535
+ };
536
+
537
+ $scope.$watch('$ctrl.tabs[$ctrl.activeTab].query', (newQuery) => {
538
+ const activeTab = $ctrl.tabs[$ctrl.activeTab];
539
+
540
+ if (!activeTab) {
541
+ return;
542
+ }
543
+
544
+ applyAutomaticTabTitle(activeTab, $ctrl.activeTab);
545
+
546
+ $ctrl.persistTabs();
547
+ });
548
+ $scope.$watch('$ctrl.url', (newUrl) => {
549
+ debounce(() => {
550
+ loadSchema(newUrl);
551
+ sessionStorage.setItem('url', newUrl);
552
+ $scope.$apply();
553
+ }, 500)();
554
+ });
555
+ // Carrega as abas do sessionStorage
556
+ function loadTabs() {
557
+ const storedTabs = TabService.getTabs();
558
+
559
+ // Garante que cada aba tenha um ID único (evita duplicatas no ngRepeat)
560
+ setTabs(storedTabs);
561
+
562
+ const storedActiveTab = parseInt(sessionStorage.getItem(ACTIVE_TAB_STORAGE_KEY), 10);
563
+ if (Number.isInteger(storedActiveTab) && storedActiveTab >= 0) {
564
+ $ctrl.activeTab = Math.min(storedActiveTab, Math.max($ctrl.tabs.length - 1, 0));
565
+ }
566
+ }
567
+ function loadSchema(newUrl) {
568
+ console.log('Loading schema from', newUrl);
569
+ $ctrl.loadingSchema = true;
570
+ $ctrl.loadSchemaError = null;
571
+ const typeRefFragment = `
572
+ name
573
+ kind
574
+ ofType {
575
+ name
576
+ kind
577
+ ofType {
578
+ name
579
+ kind
580
+ ofType {
581
+ name
582
+ kind
583
+ ofType {
584
+ name
585
+ kind
586
+ ofType {
587
+ name
588
+ kind
589
+ ofType {
590
+ name
591
+ kind
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+ `;
599
+
600
+ fetch(newUrl, {
601
+ method: 'POST',
602
+ headers: { 'Content-Type': 'application/json' },
603
+ body: JSON.stringify({
604
+ query: `
605
+ {
606
+ __schema {
607
+ description
608
+ queryType { name }
609
+ mutationType { name }
610
+ subscriptionType { name }
611
+
612
+ types {
613
+ name
614
+ description
615
+ kind
616
+ enumValues {
617
+ name
618
+ description
619
+ }
620
+ possibleTypes {
621
+ name
622
+ kind
623
+ }
624
+ inputFields {
625
+ name
626
+ description
627
+ defaultValue
628
+ type { ${typeRefFragment} }
629
+ }
630
+ fields {
631
+ name description
632
+ args {
633
+ name
634
+ description
635
+ defaultValue
636
+ type { ${typeRefFragment} }
637
+ }
638
+ type { ${typeRefFragment} }
639
+ }
640
+ }
641
+ }
642
+ }
643
+ ` })
644
+ })
645
+ .then(res => res.json())
646
+ .then(result => {
647
+ $ctrl.schema = result.data.__schema;
648
+ $ctrl.loadSchemaError = null;
649
+ console.log('Schema loaded:', $ctrl.schema);
650
+ $timeout(() => {
651
+ $scope.$apply();
652
+ }, 0);
653
+ })
654
+ .catch(err => {
655
+ console.error('Error loading schema:', err);
656
+ $ctrl.schema = null;
657
+ $ctrl.loadSchemaError = 'Failed to load schema: ' + err.message;
658
+ $timeout(() => {
659
+ $scope.$apply();
660
+ }, 0);
661
+ })
662
+ .finally(() => {
663
+ $ctrl.loadingSchema = false;
664
+ });
665
+ }
666
+ $ctrl.loadSchema = function () {
667
+ loadSchema($ctrl.url);
668
+ };
669
+
670
+ function parseOperationSnippet(queryText) {
671
+ const text = queryText || '';
672
+ const operationMatch = /^(query|mutation|subscription)\b\s*(\([^)]*\))?\s*\{/.exec(text.trim());
673
+
674
+ if (!operationMatch) {
675
+ return null;
676
+ }
677
+
678
+ const operation = operationMatch[1];
679
+ const variableBlock = operationMatch[2] || '';
680
+ const bodyStart = text.indexOf('{');
681
+ const bodyEnd = text.lastIndexOf('}');
682
+
683
+ if (bodyStart === -1 || bodyEnd === -1 || bodyEnd <= bodyStart) {
684
+ return null;
685
+ }
686
+
687
+ const body = text.slice(bodyStart + 1, bodyEnd).trim();
688
+ const fieldMatch = /^([A-Za-z_][A-Za-z0-9_]*)/.exec(body);
689
+
690
+ return {
691
+ operation,
692
+ variableBlock,
693
+ body,
694
+ fieldName: fieldMatch ? fieldMatch[1] : null
695
+ };
696
+ }
697
+
698
+ function mergeOperationIntoQuery(existingQuery, snippetQuery) {
699
+ const currentQuery = existingQuery || '';
700
+ const snippetInfo = parseOperationSnippet(snippetQuery);
701
+
702
+ if (!snippetInfo) {
703
+ return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
704
+ }
705
+
706
+ const operationRegex = new RegExp(`(^|\\n)\\s*${snippetInfo.operation}\\b([\\s\\S]*?)\\{`, 'm');
707
+ const operationMatch = operationRegex.exec(currentQuery);
708
+
709
+ if (!operationMatch) {
710
+ return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
711
+ }
712
+
713
+ const operationStart = operationMatch.index + operationMatch[1].length;
714
+ const headerStart = currentQuery.indexOf(snippetInfo.operation, operationStart);
715
+ const openBraceIndex = currentQuery.indexOf('{', headerStart);
716
+
717
+ if (openBraceIndex === -1) {
718
+ return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
719
+ }
720
+
721
+ let depth = 0;
722
+ let closeBraceIndex = -1;
723
+
724
+ for (let index = openBraceIndex; index < currentQuery.length; index += 1) {
725
+ const char = currentQuery[index];
726
+
727
+ if (char === '{') {
728
+ depth += 1;
729
+ } else if (char === '}') {
730
+ depth -= 1;
731
+
732
+ if (depth === 0) {
733
+ closeBraceIndex = index;
734
+ break;
735
+ }
736
+ }
737
+ }
738
+
739
+ if (closeBraceIndex === -1) {
740
+ return currentQuery ? `${currentQuery.replace(/\s+$/, '')}\n\n${snippetQuery}` : snippetQuery;
741
+ }
742
+
743
+ const operationBlock = currentQuery.slice(headerStart, closeBraceIndex + 1);
744
+
745
+ if (snippetInfo.fieldName && new RegExp(`\\b${snippetInfo.fieldName}\\b`).test(operationBlock)) {
746
+ return currentQuery;
747
+ }
748
+
749
+ let updatedHeader = currentQuery.slice(headerStart, openBraceIndex);
750
+ const snippetVariables = (snippetInfo.variableBlock || '').trim();
751
+
752
+ if (snippetVariables) {
753
+ const existingVariableMatch = /\(([^)]*)\)\s*$/.exec(updatedHeader);
754
+
755
+ if (existingVariableMatch) {
756
+ const existingVariables = existingVariableMatch[1].trim();
757
+ const snippetVariablesContent = snippetVariables.slice(1, -1).trim();
758
+ const existingDefinitions = existingVariables ? existingVariables.split(/\s*,\s*/) : [];
759
+ const nextDefinitions = [...existingDefinitions];
760
+
761
+ snippetVariablesContent.split(/\s*,\s*/).filter(Boolean).forEach((definition) => {
762
+ if (!nextDefinitions.includes(definition)) {
763
+ nextDefinitions.push(definition);
764
+ }
765
+ });
766
+
767
+ updatedHeader = updatedHeader.replace(/\(([^)]*)\)\s*$/, `(${nextDefinitions.join(', ')}) `);
768
+ } else {
769
+ updatedHeader = `${updatedHeader.trimEnd()} ${snippetVariables} `;
770
+ }
771
+ }
772
+
773
+ const operationBody = currentQuery.slice(openBraceIndex + 1, closeBraceIndex).replace(/\s+$/, '');
774
+ const nextBody = operationBody
775
+ ? `${operationBody}\n ${snippetInfo.body}`
776
+ : `\n ${snippetInfo.body}`;
777
+
778
+ return `${currentQuery.slice(0, headerStart)}${updatedHeader}{${nextBody}\n}${currentQuery.slice(closeBraceIndex + 1)}`;
779
+ }
780
+
781
+ function mergeVariablesJson(existingVariablesText, snippetVariablesText) {
782
+ try {
783
+ const existing = JSON.parse(existingVariablesText || '{}');
784
+ const incoming = JSON.parse(snippetVariablesText || '{}');
785
+
786
+ if (!existing || Array.isArray(existing) || typeof existing !== 'object') {
787
+ return snippetVariablesText;
788
+ }
789
+
790
+ if (!incoming || Array.isArray(incoming) || typeof incoming !== 'object') {
791
+ return existingVariablesText;
792
+ }
793
+
794
+ return JSON.stringify({ ...incoming, ...existing }, null, 2);
795
+ } catch (error) {
796
+ return existingVariablesText || snippetVariablesText;
797
+ }
798
+ }
799
+
800
+ $ctrl.insertSchemaOperation = function (snippet) {
801
+ const activeTab = $ctrl.tabs[$ctrl.activeTab];
802
+
803
+ if (!activeTab || !snippet || !snippet.query) {
804
+ return;
805
+ }
806
+
807
+ const currentQuery = (activeTab.query || '').trim();
808
+ const isPlaceholderQuery = !currentQuery || currentQuery === $ctrl.t('query.placeholder');
809
+
810
+ const nextQuery = isPlaceholderQuery
811
+ ? snippet.query
812
+ : mergeOperationIntoQuery(activeTab.query, snippet.query);
813
+ const nextVariables = snippet.variables
814
+ ? mergeVariablesJson(activeTab.variables, snippet.variables)
815
+ : activeTab.variables;
816
+
817
+ $ctrl.tabs[$ctrl.activeTab] = {
818
+ ...activeTab,
819
+ query: nextQuery,
820
+ variables: nextVariables
821
+ };
822
+
823
+ $ctrl.persistTabs();
824
+
825
+ $timeout(() => {
826
+ $scope.$applyAsync();
827
+ }, 0);
828
+ };
829
+
830
+ $ctrl.send = async function () {
831
+ const activeTab = $ctrl.tabs[$ctrl.activeTab];
832
+
833
+ if (!activeTab) {
834
+ return;
835
+ }
836
+
837
+ const rawQuery = typeof activeTab.query === 'string' ? activeTab.query.trim() : '';
838
+ if (!rawQuery || rawQuery === $ctrl.t('query.placeholder')) {
839
+ activeTab.result = JSON.stringify({ error: 'Query is empty.' }, null, 2);
840
+ $ctrl.persistTabs();
841
+ return;
842
+ }
843
+
844
+ activeTab.result = 'Loading...';
845
+ $ctrl.persistTabs();
846
+
847
+ try {
848
+ const variables = parseJsonObject(activeTab.variables, {});
849
+ const requestHeaders = parseJsonObject(activeTab.headers, {});
850
+ const mergedHeaders = {
851
+ 'Content-Type': 'application/json',
852
+ ...getSharedHeadersMap(),
853
+ ...requestHeaders
854
+ };
855
+
856
+ const response = await fetch($ctrl.url, {
857
+ method: 'POST',
858
+ headers: mergedHeaders,
859
+ body: JSON.stringify({
860
+ query: activeTab.query,
861
+ variables
862
+ })
863
+ });
864
+
865
+ const formattedBody = await formatResponseBody(response, {
866
+ status: response.status,
867
+ statusText: response.statusText
868
+ });
869
+
870
+ activeTab.result = formattedBody;
871
+ } catch (error) {
872
+ activeTab.result = JSON.stringify({
873
+ error: error && error.message ? error.message : 'Request failed.'
874
+ }, null, 2);
875
+ }
876
+
877
+ $ctrl.persistTabs();
878
+ $timeout(() => {
879
+ $scope.$applyAsync();
880
+ }, 0);
881
+ };
882
+
883
+ $ctrl.downloadWorkspace = function () {
884
+ const exportPayload = {
885
+ version: 1,
886
+ exportedAt: new Date().toISOString(),
887
+ locale: I18nService.getLocale(),
888
+ url: $ctrl.url,
889
+ activeTab: $ctrl.activeTab,
890
+ sharedHeaders: sanitizeSharedHeadersForExport(AppState.getKey(SHARED_HEADERS_STORAGE_KEY) || []),
891
+ tabs: ($ctrl.tabs || []).map((tab) => ({
892
+ id: tab.id,
893
+ title: tab.title,
894
+ userDefinedLabel: tab.userDefinedLabel || '',
895
+ query: tab.query || '',
896
+ variables: tab.variables || '{}',
897
+ headers: sanitizeHeadersTextForExport(tab.headers),
898
+ result: tab.result || ''
899
+ }))
900
+ };
901
+
902
+ const json = JSON.stringify(exportPayload, null, 2);
903
+ const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
904
+ const objectUrl = URL.createObjectURL(blob);
905
+ const link = document.createElement('a');
906
+
907
+ link.href = objectUrl;
908
+ link.download = 'graphql-playground-workspace.json';
909
+ document.body.appendChild(link);
910
+ link.click();
911
+ document.body.removeChild(link);
912
+ URL.revokeObjectURL(objectUrl);
913
+ };
914
+
915
+ $ctrl.importWorkspace = function (workspace) {
916
+ if (!workspace || typeof workspace !== 'object') {
917
+ throw new Error(getWorkspaceLabel('importError'));
918
+ }
919
+
920
+ const nextLocale = typeof workspace.locale === 'string' ? workspace.locale : I18nService.getLocale();
921
+ $ctrl.locale = I18nService.setLocale(nextLocale);
922
+
923
+ if (Array.isArray(workspace.sharedHeaders)) {
924
+ AppState.saveKey(SHARED_HEADERS_STORAGE_KEY, workspace.sharedHeaders.map((header) => ({
925
+ key: header && typeof header.key === 'string' ? header.key : '',
926
+ value: header && typeof header.value === 'string' ? header.value : ''
927
+ })));
928
+ }
929
+
930
+ setTabs(Array.isArray(workspace.tabs) ? workspace.tabs : []);
931
+ $ctrl.activeTab = Number.isInteger(workspace.activeTab)
932
+ ? Math.min(Math.max(workspace.activeTab, 0), Math.max($ctrl.tabs.length - 1, 0))
933
+ : 0;
934
+ $ctrl.url = typeof workspace.url === 'string' && workspace.url.trim()
935
+ ? workspace.url
936
+ : $ctrl.url;
937
+
938
+ sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
939
+ sessionStorage.setItem('url', $ctrl.url);
940
+ TabService.saveTabs($ctrl.tabs);
941
+ $ctrl.showConfig = false;
942
+ loadSchema($ctrl.url);
943
+ $ctrl.persistTabs();
944
+
945
+ $timeout(() => {
946
+ $scope.$applyAsync();
947
+ }, 0);
948
+ };
949
+
950
+ // Adiciona uma nova aba
951
+ $ctrl.addTab = function () {
952
+ const newTab = {
953
+ id: TabService.generateTabId(),
954
+ title: `${$ctrl.t('tabs.default')} ${$ctrl.tabs.length}`,
955
+ userDefinedLabel: '',
956
+ query: $ctrl.t('query.placeholder'),
957
+ variables: '{}',
958
+ headers: '{}',
959
+ };
960
+
961
+ $ctrl.tabs.push(newTab);
962
+ TabService.saveTabs($ctrl.tabs);
963
+ };
964
+
965
+ // Fecha uma aba
966
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
967
+ $ctrl.closeTab = function (index) {
968
+ if ($ctrl.tabs.length <= 1) {
969
+ return;
970
+ }
971
+
972
+ $ctrl.tabs.splice(index, 1);
973
+ $ctrl.activeTab = clamp($ctrl.activeTab, 0, $ctrl.tabs.length - 1);
974
+ sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String($ctrl.activeTab || 0));
975
+ TabService.saveTabs($ctrl.tabs);
976
+ };
977
+
978
+ // Inicializa abas carregadas do sessionStorage
979
+ loadTabs();
980
+ if ($ctrl.tabs.length === 0) {
981
+ $ctrl.addTab();
982
+ }
983
+
984
+ $scope.$watch('$ctrl.activeTab', (newActiveTab) => {
985
+ if (!Number.isInteger(newActiveTab) || newActiveTab < 0) {
986
+ return;
987
+ }
988
+
989
+ sessionStorage.setItem(ACTIVE_TAB_STORAGE_KEY, String(newActiveTab));
990
+ refreshActiveTabEditors();
991
+ });
992
+
993
+ // Funções auxiliares para atalhos de teclado
994
+ const handleKeyHelpers = {
995
+ isCtrlSpace: (event) => event.which === 32 && event.ctrlKey,
996
+ };
997
+
998
+ // Manipuladores de eventos de teclado
999
+ function handleKeyEvent(event, type) {
1000
+ if (handleKeyHelpers.isCtrlSpace(event)) {
1001
+ console.log('ctrl+space (autocomplete for)', type);
1002
+ // TODO: Implement autocomplete
1003
+ } else {
1004
+ // console.log(`${type} keydown:`, event.key, event.keyCode, event.keyChar, event);
1005
+ }
1006
+ }
1007
+
1008
+ $ctrl.handleQueryKeydown = (event) => handleKeyEvent(event, 'Query');
1009
+ $ctrl.handleVariablesKeydown = (event) => handleKeyEvent(event, 'Variables');
1010
+ $ctrl.handleHeadersKeydown = (event) => handleKeyEvent(event, 'Headers');
1011
+
1012
+ // Garante a sincronização do estado com o AngularJS caso necessário
1013
+ $timeout(() => {
1014
+ $scope.$apply();
1015
+ }, 0);
1016
+ }]);