@quickql/server 1.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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/core/src/client.d.ts +57 -0
- package/dist/core/src/client.d.ts.map +1 -0
- package/dist/core/src/client.js +164 -0
- package/dist/core/src/client.js.map +1 -0
- package/dist/core/src/index.d.ts +13 -0
- package/dist/core/src/index.d.ts.map +1 -0
- package/dist/core/src/index.js +13 -0
- package/dist/core/src/index.js.map +1 -0
- package/dist/core/src/types.d.ts +82 -0
- package/dist/core/src/types.d.ts.map +1 -0
- package/dist/core/src/types.js +12 -0
- package/dist/core/src/types.js.map +1 -0
- package/dist/server/src/engine.d.ts +55 -0
- package/dist/server/src/engine.d.ts.map +1 -0
- package/dist/server/src/engine.js +422 -0
- package/dist/server/src/engine.js.map +1 -0
- package/dist/server/src/errors.d.ts +31 -0
- package/dist/server/src/errors.d.ts.map +1 -0
- package/dist/server/src/errors.js +73 -0
- package/dist/server/src/errors.js.map +1 -0
- package/dist/server/src/executor.d.ts +25 -0
- package/dist/server/src/executor.d.ts.map +1 -0
- package/dist/server/src/executor.js +121 -0
- package/dist/server/src/executor.js.map +1 -0
- package/dist/server/src/filters.d.ts +10 -0
- package/dist/server/src/filters.d.ts.map +1 -0
- package/dist/server/src/filters.js +47 -0
- package/dist/server/src/filters.js.map +1 -0
- package/dist/server/src/handler.d.ts +12 -0
- package/dist/server/src/handler.d.ts.map +1 -0
- package/dist/server/src/handler.js +138 -0
- package/dist/server/src/handler.js.map +1 -0
- package/dist/server/src/index.d.ts +25 -0
- package/dist/server/src/index.d.ts.map +1 -0
- package/dist/server/src/index.js +26 -0
- package/dist/server/src/index.js.map +1 -0
- package/dist/server/src/parser/parser.d.ts +21 -0
- package/dist/server/src/parser/parser.d.ts.map +1 -0
- package/dist/server/src/parser/parser.js +99 -0
- package/dist/server/src/parser/parser.js.map +1 -0
- package/dist/server/src/parser/tokenizer.d.ts +18 -0
- package/dist/server/src/parser/tokenizer.d.ts.map +1 -0
- package/dist/server/src/parser/tokenizer.js +94 -0
- package/dist/server/src/parser/tokenizer.js.map +1 -0
- package/dist/server/src/parser/transformer.d.ts +24 -0
- package/dist/server/src/parser/transformer.d.ts.map +1 -0
- package/dist/server/src/parser/transformer.js +61 -0
- package/dist/server/src/parser/transformer.js.map +1 -0
- package/dist/server/src/parser/types.d.ts +21 -0
- package/dist/server/src/parser/types.d.ts.map +1 -0
- package/dist/server/src/parser/types.js +11 -0
- package/dist/server/src/parser/types.js.map +1 -0
- package/dist/server/src/playground/assets.d.ts +16 -0
- package/dist/server/src/playground/assets.d.ts.map +1 -0
- package/dist/server/src/playground/assets.js +1113 -0
- package/dist/server/src/playground/assets.js.map +1 -0
- package/dist/server/src/playground/docs.d.ts +15 -0
- package/dist/server/src/playground/docs.d.ts.map +1 -0
- package/dist/server/src/playground/docs.js +223 -0
- package/dist/server/src/playground/docs.js.map +1 -0
- package/dist/server/src/playground/html.d.ts +20 -0
- package/dist/server/src/playground/html.d.ts.map +1 -0
- package/dist/server/src/playground/html.js +50 -0
- package/dist/server/src/playground/html.js.map +1 -0
- package/dist/server/src/plugins/index.d.ts +21 -0
- package/dist/server/src/plugins/index.d.ts.map +1 -0
- package/dist/server/src/plugins/index.js +38 -0
- package/dist/server/src/plugins/index.js.map +1 -0
- package/dist/server/src/relations.d.ts +22 -0
- package/dist/server/src/relations.d.ts.map +1 -0
- package/dist/server/src/relations.js +98 -0
- package/dist/server/src/relations.js.map +1 -0
- package/dist/server/src/resolver/generator.d.ts +25 -0
- package/dist/server/src/resolver/generator.d.ts.map +1 -0
- package/dist/server/src/resolver/generator.js +65 -0
- package/dist/server/src/resolver/generator.js.map +1 -0
- package/dist/server/src/resolver/mapper.d.ts +23 -0
- package/dist/server/src/resolver/mapper.d.ts.map +1 -0
- package/dist/server/src/resolver/mapper.js +96 -0
- package/dist/server/src/resolver/mapper.js.map +1 -0
- package/dist/server/src/schema/builder.d.ts +21 -0
- package/dist/server/src/schema/builder.d.ts.map +1 -0
- package/dist/server/src/schema/builder.js +23 -0
- package/dist/server/src/schema/builder.js.map +1 -0
- package/dist/server/src/schema/types.d.ts +81 -0
- package/dist/server/src/schema/types.d.ts.map +1 -0
- package/dist/server/src/schema/types.js +11 -0
- package/dist/server/src/schema/types.js.map +1 -0
- package/dist/server/src/schema/utils.d.ts +16 -0
- package/dist/server/src/schema/utils.d.ts.map +1 -0
- package/dist/server/src/schema/utils.js +42 -0
- package/dist/server/src/schema/utils.js.map +1 -0
- package/dist/server/src/security.d.ts +46 -0
- package/dist/server/src/security.d.ts.map +1 -0
- package/dist/server/src/security.js +189 -0
- package/dist/server/src/security.js.map +1 -0
- package/dist/server/src/types.d.ts +158 -0
- package/dist/server/src/types.d.ts.map +1 -0
- package/dist/server/src/types.js +11 -0
- package/dist/server/src/types.js.map +1 -0
- package/dist/server/src/validator.d.ts +30 -0
- package/dist/server/src/validator.d.ts.map +1 -0
- package/dist/server/src/validator.js +171 -0
- package/dist/server/src/validator.js.map +1 -0
- package/package.json +25 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL Playground Assets
|
|
3
|
+
*
|
|
4
|
+
* Contains the bundled JavaScript and documentation logic for the
|
|
5
|
+
* browser-based QuickQL Console. Managed via static CDN in production.
|
|
6
|
+
*
|
|
7
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
8
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
import { docsJs } from './docs';
|
|
12
|
+
/**
|
|
13
|
+
* SUPREME LIGHT Logic for the QuickQL Console.
|
|
14
|
+
*/
|
|
15
|
+
export const playgroundJs = `
|
|
16
|
+
${docsJs}
|
|
17
|
+
(function() {
|
|
18
|
+
const config = (window).QuickQLConfig || { endpoint: '/', debug: false };
|
|
19
|
+
|
|
20
|
+
// UI Architecture
|
|
21
|
+
const app = document.getElementById('app');
|
|
22
|
+
if (app) app.innerHTML = \`
|
|
23
|
+
<div class="d-flex" style="height: 100vh; background: #f8fafc;">
|
|
24
|
+
<!-- SIDEBAR -->
|
|
25
|
+
<aside class="sidebar-supreme bg-white border-end shadow-sm d-flex flex-column flex-shrink-0" style="width: 280px; z-index: 100;">
|
|
26
|
+
<div class="p-4 border-bottom bg-white d-flex align-items-center justify-content-between" style="height: 72px;">
|
|
27
|
+
<img src="https://cdn-static.udinmo.com/content/logo/quickql.png" alt="QuickQL" style="height: 28px; width: auto;">
|
|
28
|
+
<button id="info-btn" class="btn btn-link p-0 text-muted opacity-25 hover-opacity-100" data-bs-toggle="modal" data-bs-target="#info-modal">
|
|
29
|
+
<i class="fas fa-info-circle"></i>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="sidebar-content flex-grow-1 p-0 d-flex flex-column" style="overflow: hidden;">
|
|
33
|
+
<div class="px-4 pt-3 pb-0 d-flex align-items-center justify-content-between">
|
|
34
|
+
<label class="text-uppercase small fw-bold text-muted mb-0" style="letter-spacing: 2px; font-size: 10px;">Explorer</label>
|
|
35
|
+
<div class="d-flex gap-2">
|
|
36
|
+
<button class="btn btn-link btn-sm p-0 text-muted opacity-50 hover-opacity-100" id="expand-all" title="Expand All"><i class="fas fa-layer-group"></i></button>
|
|
37
|
+
<button class="btn btn-link btn-sm p-0 text-muted opacity-50 hover-opacity-100" id="collapse-all" title="Collapse All"><i class="fas fa-compress-alt"></i></button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="p-3">
|
|
42
|
+
<div class="input-group input-group-sm bg-light rounded-3 px-2 border" style="border-radius: 8px !important;">
|
|
43
|
+
<span class="input-group-text bg-transparent border-0 text-muted"><i class="fas fa-search x-small"></i></span>
|
|
44
|
+
<input type="text" id="schema-search" class="form-control bg-transparent border-0 x-small" placeholder="Filter collections..." style="box-shadow: none;">
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div id="schema-list" class="flex-grow-1 overflow-auto d-flex flex-column">
|
|
49
|
+
<div class="text-center p-5 opacity-25"><i class="fas fa-spinner fa-spin"></i></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="p-3 border-top bg-light">
|
|
54
|
+
<div class="d-flex align-items-center justify-content-between small">
|
|
55
|
+
<span class="text-muted"><i class="fas fa-signal me-1 text-success"></i> Connected</span>
|
|
56
|
+
<span class="badge bg-white text-dark border fw-bold">LOCAL</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</aside>
|
|
60
|
+
|
|
61
|
+
<!-- MAIN MAIN -->
|
|
62
|
+
<main class="flex-grow-1 d-flex flex-column h-100" style="overflow: hidden; min-width: 0;">
|
|
63
|
+
<nav class="navbar navbar-expand-lg bg-white border-bottom px-4 flex-shrink-0" style="height: 72px;">
|
|
64
|
+
<div class="container-fluid p-0 d-flex justify-content-between align-items-center flex-nowrap">
|
|
65
|
+
<div class="d-flex align-items-center gap-4">
|
|
66
|
+
<button id="run-btn" class="btn btn-primary d-flex align-items-center gap-2 px-4 fw-bold shadow-sm" style="border-radius: 12px; height: 44px; transition: all 0.2s;">
|
|
67
|
+
<i class="fas fa-play"></i> Run
|
|
68
|
+
</button>
|
|
69
|
+
<div id="status-chip" class="badge rounded-pill bg-light text-secondary px-3 py-2 border" style="font-size: 11px; font-weight: 600;">
|
|
70
|
+
<i class="fas fa-circle me-1" style="font-size: 8px;"></i> READY
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="d-flex align-items-center gap-3">
|
|
75
|
+
<div class="d-flex bg-light p-1 rounded-pill border mx-3 shadow-sm" style="font-size: 11px;">
|
|
76
|
+
<button class="nav-view-btn active border-0 px-3 py-1 rounded-pill" data-view="playground">
|
|
77
|
+
<i class="fas fa-terminal me-1"></i> <span class="nav-btn-text">Playground</span>
|
|
78
|
+
</button>
|
|
79
|
+
<button class="nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white" data-view="schema">
|
|
80
|
+
<i class="fas fa-project-diagram me-1"></i> <span class="nav-btn-text">Schema</span>
|
|
81
|
+
</button>
|
|
82
|
+
<button class="nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white" data-view="visualizer">
|
|
83
|
+
<i class="fas fa-cube me-1"></i> <span class="nav-btn-text">Visualizer</span>
|
|
84
|
+
</button>
|
|
85
|
+
<button class="nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white" data-view="docs">
|
|
86
|
+
<i class="fas fa-book-open me-1"></i> <span class="nav-btn-text">Docs</span>
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div id="perf-timer" class="text-muted small fw-bold font-monospace bg-light px-3 py-1 rounded-pill">--- ms</div>
|
|
90
|
+
<div class="vr mx-2 text-muted opacity-25"></div>
|
|
91
|
+
<button id="settings-btn" class="btn btn-light btn-sm rounded-circle shadow-sm" style="width: 36px; height: 36px;" data-bs-toggle="modal" data-bs-target="#settings-modal">
|
|
92
|
+
<i class="fas fa-cog text-muted"></i>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</nav>
|
|
97
|
+
|
|
98
|
+
<!-- WORKSPACE CONTENT AREA -->
|
|
99
|
+
<div id="workspace-container" class="flex-grow-1 overflow-hidden bg-white position-relative" style="min-width: 0;">
|
|
100
|
+
<div id="loader" class="position-absolute top-50 start-50 translate-middle d-none text-muted">
|
|
101
|
+
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
102
|
+
</div>
|
|
103
|
+
<div id="workspace-content" class="h-100" style="min-width: 0;">
|
|
104
|
+
<!-- Dynamic content injected here -->
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</main>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- SETTINGS MODAL -->
|
|
111
|
+
<div class="modal fade" id="settings-modal" tabindex="-1" aria-hidden="true">
|
|
112
|
+
<div class="modal-dialog modal-dialog-centered modal-lg">
|
|
113
|
+
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px; overflow: hidden; background: #ffffff;">
|
|
114
|
+
<div class="modal-header border-0 p-4" style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);">
|
|
115
|
+
<div class="d-flex align-items-center gap-3">
|
|
116
|
+
<div class="bg-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 48px; height: 48px;">
|
|
117
|
+
<i class="fas fa-sliders-h text-primary fs-5"></i>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<h5 class="modal-title fw-bolder mb-0 text-dark" style="letter-spacing: -0.5px;">Console Settings</h5>
|
|
121
|
+
<p class="text-muted small mb-0">Configuration and global preferences</p>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<button type="button" class="btn-close shadow-none" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="modal-body p-5">
|
|
127
|
+
<div class="mb-4">
|
|
128
|
+
<label class="text-uppercase small fw-bold text-muted mb-3 d-flex align-items-center gap-2"><i class="fas fa-code text-primary"></i> Environment Variables</label>
|
|
129
|
+
<div id="config-json" class="p-4 bg-dark text-light border-0 rounded-4 font-monospace overflow-auto" style="max-height: 300px; font-size: 13px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);"></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="row g-4">
|
|
132
|
+
<div class="col-md-6">
|
|
133
|
+
<div class="p-4 border-0 rounded-4 bg-light shadow-sm h-100" style="background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);">
|
|
134
|
+
<div class="d-flex align-items-center gap-3 mb-2">
|
|
135
|
+
<div class="bg-white p-2 rounded-3 shadow-sm"><i class="fas fa-link text-primary"></i></div>
|
|
136
|
+
<div class="small text-muted fw-bold text-uppercase">ENDPOINT</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="fw-bold fs-5 text-dark font-monospace text-truncate" id="config-endpoint">--</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="col-md-6">
|
|
142
|
+
<div class="p-4 border-0 rounded-4 bg-light shadow-sm h-100" style="background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);">
|
|
143
|
+
<div class="d-flex align-items-center gap-3 mb-2">
|
|
144
|
+
<div class="bg-white p-2 rounded-3 shadow-sm"><i class="fas fa-power-off text-success"></i></div>
|
|
145
|
+
<div class="small text-muted fw-bold text-uppercase">MODE</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="fw-bold fs-5 text-dark font-monospace" id="config-mode">--</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="mt-4 p-4 rounded-4 border" style="background: #f8fafc; border-style: dashed !important;">
|
|
152
|
+
<div class="d-flex align-items-center justify-content-between">
|
|
153
|
+
<div class="d-flex align-items-center gap-3">
|
|
154
|
+
<div class="bg-white p-2 rounded-3 shadow-sm text-primary"><i class="fas fa-external-link-alt"></i></div>
|
|
155
|
+
<div>
|
|
156
|
+
<div class="fw-bold text-dark" style="font-size: 14px;">Official QuickQL Documentation</div>
|
|
157
|
+
<div class="text-muted small">View full API specs, ecosystem guides, and examples.</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<a href="https://docs.udinmo.com" target="_blank" class="btn btn-outline-primary btn-sm fw-bold px-3" style="border-radius: 8px;">Open Docs</a>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="modal-footer border-0 p-4 bg-light">
|
|
165
|
+
<button type="button" class="btn btn-primary fw-bold px-5 py-2 shadow-sm" style="border-radius: 12px;" data-bs-dismiss="modal">Save & Close</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- INFO MODAL -->
|
|
172
|
+
<div class="modal fade" id="info-modal" tabindex="-1" aria-hidden="true">
|
|
173
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
174
|
+
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px; overflow: hidden; background: linear-gradient(145deg, #1e293b, #0f172a);">
|
|
175
|
+
<div class="modal-body p-5 text-center text-white position-relative">
|
|
176
|
+
<div class="position-absolute top-0 start-50 translate-middle-x w-100" style="height: 4px; background: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899);"></div>
|
|
177
|
+
<img src="https://cdn-static.udinmo.com/content/logo/quickql.png" class="mb-4" style="height: 48px; filter: brightness(0) invert(1);">
|
|
178
|
+
<h4 class="fw-bold mb-2">QuickQL Console</h4>
|
|
179
|
+
<p class="text-white-50 small mb-4">The supreme data exploration tool.</p>
|
|
180
|
+
<div class="badge bg-white bg-opacity-10 text-white mb-4 px-4 py-2 rounded-pill border border-white border-opacity-25" style="box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">v1.2.4 — \${config.debug ? 'Debug' : 'Production'} Mode</div>
|
|
181
|
+
|
|
182
|
+
<div class="mb-4 text-start">
|
|
183
|
+
<label class="x-small fw-bold text-white-50 mb-2 text-uppercase" style="letter-spacing: 1.5px;"><i class="fas fa-layer-group me-1"></i> Runtime Configuration</label>
|
|
184
|
+
<pre id="info-config-json" class="p-3 bg-black bg-opacity-25 border border-white border-opacity-10 rounded-4 text-start text-white-50 font-monospace mb-0" style="font-size: 11px; max-height: 150px; overflow: auto;"></pre>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="d-flex justify-content-center gap-3">
|
|
188
|
+
<button type="button" class="btn btn-primary px-5 py-2 rounded-pill fw-bold shadow" style="background: linear-gradient(90deg, #3b82f6, #6366f1);" data-bs-dismiss="modal">Launch Console</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
\`;
|
|
195
|
+
|
|
196
|
+
// Hydrate Modals
|
|
197
|
+
const cfgJson = document.getElementById('config-json');
|
|
198
|
+
if (cfgJson) cfgJson.innerText = JSON.stringify(config, null, 2);
|
|
199
|
+
const cfgEnd = document.getElementById('config-endpoint');
|
|
200
|
+
if (cfgEnd) cfgEnd.innerText = config.endpoint || '/';
|
|
201
|
+
const cfgMode = document.getElementById('config-mode');
|
|
202
|
+
if (cfgMode) cfgMode.innerText = config.debug ? 'Debug' : 'Production';
|
|
203
|
+
const cfgInfo = document.getElementById('info-config-json');
|
|
204
|
+
if (cfgInfo) cfgInfo.innerText = JSON.stringify(config, null, 2);
|
|
205
|
+
|
|
206
|
+
// Global Navigation Listener (Event Delegation)
|
|
207
|
+
document.addEventListener('click', (e) => {
|
|
208
|
+
const target = e.target;
|
|
209
|
+
if (!target) return;
|
|
210
|
+
const btn = target.closest ? target.closest('.nav-view-btn') : null;
|
|
211
|
+
if (btn) {
|
|
212
|
+
const viewId = btn.getAttribute('data-view');
|
|
213
|
+
console.log('Navigating to:', viewId);
|
|
214
|
+
if (viewId && window.switchView) {
|
|
215
|
+
window.switchView(viewId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let editor, results, runBtn, statusChip, perfTimer, schemaList, schemaSearch, expandAllBtn, collapseAllBtn, viewButtons, detailsBtn, httpPanel, httpStatus, httpTime, httpHeaders, httpMeta, httpReqCtx, resizer, headersToggle, headersPanel, headerList, addHeaderBtn, gutter;
|
|
221
|
+
let currentView = null;
|
|
222
|
+
|
|
223
|
+
(window).playgroundState = (window).playgroundState || {
|
|
224
|
+
activeTabId: 0,
|
|
225
|
+
tabs: [
|
|
226
|
+
{ id: 0, name: 'Main Query', query: localStorage.getItem('qq_console_query') || 'users {\\n id,\\n username,\\n email\\n}', results: null }
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
(window).playgroundHistory = JSON.parse(localStorage.getItem('qq_history') || '[]');
|
|
231
|
+
|
|
232
|
+
function lookUpElements() {
|
|
233
|
+
editor = document.getElementById('editor');
|
|
234
|
+
results = document.getElementById('results');
|
|
235
|
+
runBtn = document.getElementById('run-btn');
|
|
236
|
+
statusChip = document.getElementById('status-chip');
|
|
237
|
+
perfTimer = document.getElementById('perf-timer');
|
|
238
|
+
schemaList = document.getElementById('schema-list');
|
|
239
|
+
schemaSearch = document.getElementById('schema-search');
|
|
240
|
+
expandAllBtn = document.getElementById('expand-all');
|
|
241
|
+
collapseAllBtn = document.getElementById('collapse-all');
|
|
242
|
+
viewButtons = document.querySelectorAll('.nav-view-btn');
|
|
243
|
+
detailsBtn = document.getElementById('details-btn');
|
|
244
|
+
httpPanel = document.getElementById('http-panel');
|
|
245
|
+
httpStatus = document.getElementById('http-status');
|
|
246
|
+
httpTime = document.getElementById('http-time');
|
|
247
|
+
httpHeaders = document.getElementById('http-headers');
|
|
248
|
+
httpMeta = document.getElementById('http-meta');
|
|
249
|
+
httpReqCtx = document.getElementById('http-req-ctx');
|
|
250
|
+
resizer = document.getElementById('resizer');
|
|
251
|
+
headersToggle = document.getElementById('headers-toggle');
|
|
252
|
+
headersPanel = document.getElementById('headers-panel');
|
|
253
|
+
headerList = document.getElementById('header-list');
|
|
254
|
+
addHeaderBtn = document.getElementById('add-header');
|
|
255
|
+
gutter = document.getElementById('gutter');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Routing & State Management
|
|
259
|
+
(window).switchView = async (viewId, params = {}) => {
|
|
260
|
+
const content = document.getElementById('workspace-content');
|
|
261
|
+
const loader = document.getElementById('loader');
|
|
262
|
+
if (!content) return;
|
|
263
|
+
|
|
264
|
+
// Save state if leaving playground
|
|
265
|
+
if (currentView === 'playground' && editor) {
|
|
266
|
+
const state = window.playgroundState;
|
|
267
|
+
const currentTab = state.tabs.find(t => t.id === state.activeTabId);
|
|
268
|
+
if (currentTab) currentTab.query = editor.value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update Nav UI
|
|
272
|
+
const btns = document.querySelectorAll('.nav-view-btn');
|
|
273
|
+
btns.forEach(b => {
|
|
274
|
+
const v = b.getAttribute('data-view');
|
|
275
|
+
const active = v === viewId || (viewId === 'collection' && v === 'schema');
|
|
276
|
+
b.classList.toggle('active', active);
|
|
277
|
+
b.classList.toggle('bg-white', active);
|
|
278
|
+
b.classList.toggle('shadow-sm', active);
|
|
279
|
+
b.classList.toggle('bg-transparent', !active);
|
|
280
|
+
b.classList.toggle('text-muted', !active);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (loader) loader.classList.remove('d-none');
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
if (viewId === 'playground') {
|
|
287
|
+
content.innerHTML = renderPlaygroundPage();
|
|
288
|
+
initPlaygroundLogic();
|
|
289
|
+
} else if (viewId === 'schema') {
|
|
290
|
+
content.innerHTML = await renderSchemaPage();
|
|
291
|
+
} else if (viewId === 'collection') {
|
|
292
|
+
content.innerHTML = renderCollectionPage(params.name);
|
|
293
|
+
} else if (viewId === 'visualizer') {
|
|
294
|
+
// Ensure schema is pre-fetched for the sidebar before rendering
|
|
295
|
+
if (!(window).currentSchema || (window).currentSchema.length === 0) {
|
|
296
|
+
await (window).fetchSchema();
|
|
297
|
+
}
|
|
298
|
+
content.innerHTML = renderVisualizerPage();
|
|
299
|
+
(window).initVisualizer();
|
|
300
|
+
} else if (viewId === 'docs') {
|
|
301
|
+
// Ensure schema is loaded before rendering docs
|
|
302
|
+
if (!(window).currentSchema || (window).currentSchema.length === 0) {
|
|
303
|
+
await (window).fetchSchema();
|
|
304
|
+
}
|
|
305
|
+
content.innerHTML = renderDocsPage();
|
|
306
|
+
}
|
|
307
|
+
} catch(e) {
|
|
308
|
+
console.error('Routing error:', e);
|
|
309
|
+
content.innerHTML = '<div class="p-5 text-center text-danger"><h4>Failed to load page</h4><p>' + e.message + '</p></div>';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (loader) loader.classList.add('d-none');
|
|
313
|
+
|
|
314
|
+
currentView = viewId;
|
|
315
|
+
// Push state to URL
|
|
316
|
+
if (viewId !== 'collection') {
|
|
317
|
+
if (window.location.hash !== '#' + viewId) {
|
|
318
|
+
window.location.hash = viewId;
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
if (window.location.hash !== '#schema') {
|
|
322
|
+
window.location.hash = 'schema';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// PAGE RENDERERS
|
|
328
|
+
function renderPlaygroundPage() {
|
|
329
|
+
const state = (window).playgroundState;
|
|
330
|
+
let html = '';
|
|
331
|
+
html += '<div class="d-flex flex-column h-100" style="background: #f1f5f9; min-width: 0;">';
|
|
332
|
+
html += '<div class="playground-tabs bg-white border-bottom d-flex align-items-center px-4" style="height: 48px;">';
|
|
333
|
+
html += '<div id="tabs-container" class="d-flex h-100 align-items-center">';
|
|
334
|
+
|
|
335
|
+
state.tabs.forEach(tab => {
|
|
336
|
+
const active = state.activeTabId === tab.id;
|
|
337
|
+
html += '<div class="playground-tab ' + (active ? 'active' : '') + '" ';
|
|
338
|
+
html += 'onclick="(window).switchTab(' + tab.id + ')" ';
|
|
339
|
+
html += 'style="height: 100%; display: flex; align-items: center; padding: 0 20px; font-size: 11px; font-weight: 700; color: ' + (active ? '#0d6efd' : '#64748b') + '; cursor: pointer; border-bottom: 2px solid ' + (active ? '#0d6efd' : 'transparent') + ';">';
|
|
340
|
+
html += '<i class="fas fa-file-code me-2 opacity-50"></i> ' + tab.name;
|
|
341
|
+
if (tab.id !== 0 && state.tabs.length > 1) {
|
|
342
|
+
html += '<i class="fas fa-times ms-3 opacity-25 hover-opacity-100" onclick="event.stopPropagation(); (window).closeTab(' + tab.id + ')"></i>';
|
|
343
|
+
}
|
|
344
|
+
html += '</div>';
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
html += '</div>';
|
|
348
|
+
html += '<button class="btn btn-link btn-sm text-muted p-2 ms-2 hover-bg-light rounded-circle" onclick="(window).addTab()"><i class="fas fa-plus"></i></button>';
|
|
349
|
+
html += '<div class="ms-auto d-flex align-items-center gap-3"><span class="badge bg-light text-dark border x-small fw-bold opacity-50">PRODUCTION MODE</span></div>';
|
|
350
|
+
html += '</div>';
|
|
351
|
+
|
|
352
|
+
html += '<div class="d-flex gap-4 p-4 flex-grow-1 overflow-hidden" style="min-height: 0; min-width: 0;">';
|
|
353
|
+
html += '<div class="card border-0 shadow-sm rounded-4 overflow-hidden d-flex flex-column" style="flex: 1 1 0; min-width: 0;">';
|
|
354
|
+
html += '<div class="card-header bg-white py-3 border-bottom d-flex align-items-center justify-content-between"><span class="fw-bold small text-muted"><i class="fas fa-edit me-2"></i>QUERY EDITOR</span></div>';
|
|
355
|
+
html += '<div class="card-body p-0 flex-grow-1 d-flex overflow-hidden">';
|
|
356
|
+
html += '<div class="d-flex flex-grow-1" style="background: white; min-width: 0;">';
|
|
357
|
+
html += '<div id="gutter" class="text-end text-muted font-monospace py-4 px-2 flex-shrink-0" style="width: 48px; background: #fafafa; border-right: 1px solid #f1f5f9; user-select: none; line-height: 1.6; font-size: 13px; padding-top: 1.5rem !important;">1</div>';
|
|
358
|
+
html += '<textarea id="editor" class="form-control font-monospace border-0" spellcheck="false" style="flex: 1; min-width: 0; height: 100%; resize: none; font-size: 13px; padding: 1.5rem; background: #fff; line-height: 1.6; outline: none; box-shadow: none;"></textarea>';
|
|
359
|
+
html += '</div>';
|
|
360
|
+
html += '</div>';
|
|
361
|
+
html += '<div class="card-footer bg-white border-top p-0 overflow-hidden">';
|
|
362
|
+
html += '<button id="headers-toggle" class="btn btn-white w-100 py-2 border-0 d-flex align-items-center justify-content-between px-4"><span class="fw-bold x-small text-muted">REQUEST HEADERS</span><i class="fas fa-chevron-up text-muted x-small"></i></button>';
|
|
363
|
+
html += '<div id="headers-panel" class="p-4 border-top bg-light d-none"><div id="header-list" class="d-flex flex-column gap-2 mb-3"></div><button id="add-header" class="btn btn-light btn-sm text-primary fw-bold x-small border">Add Header</button></div>';
|
|
364
|
+
html += '</div>';
|
|
365
|
+
html += '</div>';
|
|
366
|
+
|
|
367
|
+
html += '<div class="card border-0 shadow-sm rounded-4 overflow-hidden d-flex flex-column border" style="flex: 1 1 0; min-width: 0;">';
|
|
368
|
+
html += '<div class="card-header bg-white py-3 border-bottom d-flex align-items-center justify-content-between"><span class="fw-bold small text-muted"><i class="fas fa-terminal me-2"></i>SERVER RESPONSE</span></div>';
|
|
369
|
+
html += '<div class="card-body p-0 flex-grow-1 bg-light overflow-hidden d-flex flex-column" style="min-width: 0;">';
|
|
370
|
+
html += '<div id="results-container" class="flex-grow-1 overflow-auto" style="min-width: 0;"><pre id="results" class="m-0 p-4 font-monospace" style="font-size: 13px; white-space: pre-wrap; word-break: break-word;"></pre></div>';
|
|
371
|
+
html += '</div>';
|
|
372
|
+
html += '<div class="card-footer bg-white border-top p-0 overflow-hidden">';
|
|
373
|
+
html += '<button id="http-toggle" class="btn btn-white w-100 py-2 border-0 d-flex align-items-center justify-content-between px-4"><span class="fw-bold x-small text-muted"><i class="fas fa-network-wired me-2"></i>HTTP INSPECTOR</span><i class="fas fa-chevron-up text-muted x-small"></i></button>';
|
|
374
|
+
html += '<div id="http-panel" class="p-0 border-top bg-light d-none font-monospace small" style="height: 300px; overflow: hidden;">';
|
|
375
|
+
html += '<div class="d-flex h-100">';
|
|
376
|
+
html += '<div class="d-flex flex-column border-end bg-white" style="width: 220px;">';
|
|
377
|
+
html += '<div class="px-3 py-2 border-bottom fw-bold x-small text-muted bg-light d-flex justify-content-between">HISTORY <button class="btn btn-link p-0 x-small text-decoration-none" onclick="localStorage.removeItem(\\\'qq_history\\\'); (window).playgroundHistory=[]; renderHistory();">Clear</button></div>';
|
|
378
|
+
html += '<div id="history-list" class="flex-grow-1 overflow-auto p-2" style="background: #fafafa;"></div>';
|
|
379
|
+
html += '</div>';
|
|
380
|
+
html += '<div class="flex-grow-1 p-4 overflow-auto bg-white">';
|
|
381
|
+
html += '<div class="row g-3 px-2 mb-2">';
|
|
382
|
+
html += '<div class="col-6 p-0"><div class="fw-bold x-small text-muted mb-1">STATUS</div><div id="http-status" class="text-dark">--</div></div>';
|
|
383
|
+
html += '<div class="col-6 p-0"><div class="fw-bold x-small text-muted mb-1">TIME</div><div id="http-time" class="text-dark">--</div></div>';
|
|
384
|
+
html += '</div>';
|
|
385
|
+
html += '<hr class="opacity-10">';
|
|
386
|
+
html += '<div class="fw-bold x-small text-muted mb-2">RESPONSE HEADERS (incl. Cookies)</div>';
|
|
387
|
+
html += '<div id="http-headers" class="bg-white p-2 border rounded-3 text-wrap text-break mb-3" style="font-size:11px; word-break: break-all;">--</div>';
|
|
388
|
+
html += '<div class="fw-bold x-small text-muted mb-2">REQUEST PAYLOAD</div>';
|
|
389
|
+
html += '<pre id="http-meta" class="bg-white p-2 border rounded-3 m-0" style="font-size:11px; white-space: pre-wrap; word-break: break-word;">--</pre>';
|
|
390
|
+
html += '</div>';
|
|
391
|
+
html += '</div>';
|
|
392
|
+
html += '</div>';
|
|
393
|
+
html += '</div>';
|
|
394
|
+
html += '</div>';
|
|
395
|
+
html += '</div>';
|
|
396
|
+
html += '</div>';
|
|
397
|
+
|
|
398
|
+
return html;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function renderSchemaPage() {
|
|
402
|
+
const res = await fetch(config.endpoint + '?schema=true');
|
|
403
|
+
const schema = await res.json();
|
|
404
|
+
(window).currentSchema = schema;
|
|
405
|
+
|
|
406
|
+
let html = '';
|
|
407
|
+
html += '<div class="overflow-auto p-5 h-100 bg-white">';
|
|
408
|
+
html += '<div class="container-tight mx-auto" style="max-width: 1200px;">';
|
|
409
|
+
html += '<div class="d-flex align-items-center justify-content-between mb-4 pb-4 border-bottom">';
|
|
410
|
+
html += '<div><h1 class="fw-bold mb-2 text-dark">Schema Explorer</h1><p class="text-muted mb-0 lead fs-6">A structural blueprint of your data model.</p></div>';
|
|
411
|
+
html += '<button class="btn btn-primary fw-bold shadow-sm px-4 py-2 rounded-0" onclick="(window).switchView(\\'visualizer\\')"><i class="fas fa-cube me-2"></i> Visualizer</button>';
|
|
412
|
+
html += '</div>';
|
|
413
|
+
|
|
414
|
+
// Search Input
|
|
415
|
+
html += '<div class="mb-5">';
|
|
416
|
+
html += '<div class="input-group shadow-sm border border-light rounded-pill overflow-hidden bg-white">';
|
|
417
|
+
html += '<span class="input-group-text bg-transparent border-0 text-muted px-4"><i class="fas fa-search"></i></span>';
|
|
418
|
+
html += '<input type="text" id="schema-explorer-search" class="form-control border-0 bg-transparent py-3 px-3 fw-bold" placeholder="Search collections..." style="box-shadow: none; outline: none;">';
|
|
419
|
+
html += '</div>';
|
|
420
|
+
html += '</div>';
|
|
421
|
+
|
|
422
|
+
html += '<div id="schema-summary-full" class="row g-4 mb-5">';
|
|
423
|
+
|
|
424
|
+
schema.forEach(col => {
|
|
425
|
+
html += '<div class="col-md-6 col-lg-4 schema-card-container" data-name="' + col.name.toLowerCase() + '">';
|
|
426
|
+
html += '<div class="card border-0 shadow-sm rounded-0 p-4 h-100 bg-light" style="border: 1px solid #e2e8f0 !important;">';
|
|
427
|
+
html += '<div class="d-flex align-items-center justify-content-between mb-4 pb-3 border-bottom"><h4 class="fw-bold m-0 text-primary">' + col.name + '</h4><button class="btn btn-outline-primary btn-sm rounded-0 px-3 fw-bold" onclick="(window).switchView(\\'docs\\'); setTimeout(() => document.getElementById(\\'doc-' + col.name + '\\')?.scrollIntoView({behavior:\\'smooth\\'}), 100);">Docs</button></div>';
|
|
428
|
+
html += '<div class="d-flex flex-wrap gap-2">';
|
|
429
|
+
col.fields.forEach(f => {
|
|
430
|
+
html += '<span class="badge bg-white text-dark border rounded-0 px-2 py-1 font-monospace x-small shadow-sm">' + f + '</span>';
|
|
431
|
+
});
|
|
432
|
+
html += '</div>';
|
|
433
|
+
html += '</div>';
|
|
434
|
+
html += '</div>';
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
html += '</div>';
|
|
438
|
+
|
|
439
|
+
// Branding Footer
|
|
440
|
+
html += ' <div class="text-center py-5 opacity-25 mt-auto">';
|
|
441
|
+
html += ' <img src="https://cdn-static.udinmo.com/content/logo/quickql.png" style="height: 24px; filter: grayscale(1);">';
|
|
442
|
+
html += ' <p class="x-small mt-2 fw-bold text-uppercase" style="letter-spacing: 2px;">QuickQL Framework Explorer</p>';
|
|
443
|
+
html += ' </div>';
|
|
444
|
+
|
|
445
|
+
html += '</div>';
|
|
446
|
+
html += '</div>';
|
|
447
|
+
|
|
448
|
+
setTimeout(() => {
|
|
449
|
+
const searchInput = document.getElementById('schema-explorer-search');
|
|
450
|
+
if (searchInput) {
|
|
451
|
+
searchInput.addEventListener('input', (e) => {
|
|
452
|
+
const q = e.target.value.toLowerCase();
|
|
453
|
+
const cards = document.querySelectorAll('.schema-card-container');
|
|
454
|
+
cards.forEach(card => {
|
|
455
|
+
if (card.getAttribute('data-name').includes(q)) {
|
|
456
|
+
card.classList.remove('d-none');
|
|
457
|
+
} else {
|
|
458
|
+
card.classList.add('d-none');
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}, 50);
|
|
464
|
+
|
|
465
|
+
return html;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderCollectionPage(name) {
|
|
469
|
+
const schema = (window).currentSchema || [];
|
|
470
|
+
const col = schema.find(s => s.name === name);
|
|
471
|
+
if (!col) return 'Collection not found';
|
|
472
|
+
|
|
473
|
+
let html = '';
|
|
474
|
+
html += '<div class="overflow-auto p-5 h-100 bg-white">';
|
|
475
|
+
html += '<div class="container mx-auto" style="max-width: 1000px;">';
|
|
476
|
+
html += '<button class="btn btn-link p-0 text-muted mb-4" onclick="(window).switchView(\\'schema\\')">Back</button>';
|
|
477
|
+
html += '<h1 class="fw-bold mb-4">' + col.name + ' Documentation</h1>';
|
|
478
|
+
html += '<div class="card border-0 bg-light p-4 rounded-4">';
|
|
479
|
+
html += '<h5>Fields</h5>';
|
|
480
|
+
html += '<ul>';
|
|
481
|
+
col.fields.forEach(f => {
|
|
482
|
+
html += '<li>' + f + '</li>';
|
|
483
|
+
});
|
|
484
|
+
html += '</ul>';
|
|
485
|
+
html += '<button class="btn btn-dark mt-4" onclick="(window).setEditorTemplate(\\'' + col.name + '\\'); (window).switchView(\\'playground\\')">Use in Playground</button>';
|
|
486
|
+
html += '</div>';
|
|
487
|
+
html += '</div>';
|
|
488
|
+
html += '</div>';
|
|
489
|
+
return html;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function renderVisualizerPage() {
|
|
493
|
+
return \`
|
|
494
|
+
<div class="bg-light position-relative overflow-hidden h-100">
|
|
495
|
+
<div class="position-absolute top-0 start-0 p-4 z-3 d-flex gap-3">
|
|
496
|
+
<button class="btn btn-white btn-sm rounded-pill px-4 fw-bold shadow-sm border" onclick="(window).switchView('schema')">Exit</button>
|
|
497
|
+
</div>
|
|
498
|
+
<canvas id="visualizer-canvas" class="w-100 h-100"></canvas>
|
|
499
|
+
</div>
|
|
500
|
+
\`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Documentation logic imported from docs.ts
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
(window).showCollectionDocs = (name) => {
|
|
507
|
+
(window).switchView('collection', { name });
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
function initPlaygroundLogic() {
|
|
511
|
+
lookUpElements();
|
|
512
|
+
renderHistory();
|
|
513
|
+
|
|
514
|
+
if (addHeaderBtn) {
|
|
515
|
+
addHeaderBtn.onclick = () => {
|
|
516
|
+
const row = document.createElement('div');
|
|
517
|
+
row.className = 'header-row d-flex gap-2';
|
|
518
|
+
row.innerHTML = \`
|
|
519
|
+
<input type="text" class="form-control form-control-sm font-monospace x-small h-key" placeholder="Key" style="border-radius: 6px;">
|
|
520
|
+
<input type="text" class="form-control form-control-sm font-monospace x-small h-value" placeholder="Value" style="border-radius: 6px;">
|
|
521
|
+
<button class="btn btn-sm btn-link text-danger p-1 border-0 del-header"><i class="fas fa-times-circle"></i></button>
|
|
522
|
+
\`;
|
|
523
|
+
row.querySelector('.del-header')?.addEventListener('click', () => row.remove());
|
|
524
|
+
headerList?.appendChild(row);
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (headerList && headerList.children.length === 0) addHeaderBtn?.click();
|
|
529
|
+
|
|
530
|
+
if (headersToggle) {
|
|
531
|
+
headersToggle.onclick = () => {
|
|
532
|
+
headersPanel?.classList.toggle('d-none');
|
|
533
|
+
headersToggle.querySelector('.fas.fa-chevron-up')?.classList.toggle('fa-rotate-180');
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const httpToggle = document.getElementById('http-toggle');
|
|
538
|
+
if (httpToggle) {
|
|
539
|
+
httpToggle.onclick = () => {
|
|
540
|
+
httpPanel?.classList.toggle('d-none');
|
|
541
|
+
httpToggle.querySelector('.fas.fa-chevron-up')?.classList.toggle('fa-rotate-180');
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (editor) {
|
|
546
|
+
const state = window.playgroundState;
|
|
547
|
+
const currentTab = state.tabs.find(t => t.id === state.activeTabId);
|
|
548
|
+
if (currentTab) {
|
|
549
|
+
editor.value = currentTab.query;
|
|
550
|
+
if (results && currentTab.results) {
|
|
551
|
+
results.innerHTML = currentTab.results;
|
|
552
|
+
} else if (results) {
|
|
553
|
+
results.innerHTML = '<div class="p-5 text-center text-muted opacity-25">Ready to execute query</div>';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
editor.oninput = updateLineNumbers;
|
|
557
|
+
editor.onscroll = () => {
|
|
558
|
+
if (gutter) gutter.scrollTop = editor.scrollTop;
|
|
559
|
+
};
|
|
560
|
+
updateLineNumbers();
|
|
561
|
+
editor.focus();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (runBtn) runBtn.onclick = execute;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
window.switchTab = (tabId) => {
|
|
568
|
+
const state = window.playgroundState;
|
|
569
|
+
const currentTab = state.tabs.find(t => t.id === state.activeTabId);
|
|
570
|
+
if (currentTab) {
|
|
571
|
+
if (editor) currentTab.query = editor.value;
|
|
572
|
+
if (results) currentTab.results = results.innerHTML;
|
|
573
|
+
}
|
|
574
|
+
state.activeTabId = tabId;
|
|
575
|
+
window.switchView('playground');
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
window.addTab = () => {
|
|
579
|
+
const state = window.playgroundState;
|
|
580
|
+
const newId = Math.max(...state.tabs.map(t => t.id), -1) + 1;
|
|
581
|
+
state.tabs.push({
|
|
582
|
+
id: newId,
|
|
583
|
+
name: \`New Query \${newId + 1}\`,
|
|
584
|
+
query: '',
|
|
585
|
+
results: null
|
|
586
|
+
});
|
|
587
|
+
window.switchTab(newId);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
window.closeTab = (tabId) => {
|
|
591
|
+
const state = window.playgroundState;
|
|
592
|
+
if (state.tabs.length <= 1) return;
|
|
593
|
+
state.tabs = state.tabs.filter(t => t.id !== tabId);
|
|
594
|
+
if (state.activeTabId === tabId) state.activeTabId = state.tabs[0].id;
|
|
595
|
+
window.switchView('playground');
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
function updateLineNumbers() {
|
|
599
|
+
if (!editor || !gutter) return;
|
|
600
|
+
const text = editor.value;
|
|
601
|
+
const lines = text.split('\\n').length;
|
|
602
|
+
let gutterContent = '';
|
|
603
|
+
for (let i = 1; i <= lines; i++) {
|
|
604
|
+
gutterContent += '<div style="line-height: 1.6; font-size: 13px; padding: 0 8px; color: #94a3b8;">' + i + '</div>';
|
|
605
|
+
}
|
|
606
|
+
gutter.innerHTML = gutterContent;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
(window).setEditorTemplate = (model) => {
|
|
611
|
+
const query = \`\${model} {\\n id,\\n *\\n}\`;
|
|
612
|
+
const state = (window).playgroundState;
|
|
613
|
+
const currentTab = state.tabs.find(t => t.id === state.activeTabId);
|
|
614
|
+
if (currentTab) currentTab.query = query;
|
|
615
|
+
if (editor) {
|
|
616
|
+
editor.value = query;
|
|
617
|
+
updateLineNumbers();
|
|
618
|
+
editor.focus();
|
|
619
|
+
}
|
|
620
|
+
localStorage.setItem('qq_console_query', query);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
(window).fetchSchema = async () => {
|
|
624
|
+
try {
|
|
625
|
+
const res = await fetch(config.endpoint + '?schema=true');
|
|
626
|
+
const data = await res.json();
|
|
627
|
+
(window).currentSchema = data;
|
|
628
|
+
|
|
629
|
+
const schemaListEl = document.getElementById('schema-list');
|
|
630
|
+
if (!schemaListEl) return;
|
|
631
|
+
let html = '';
|
|
632
|
+
data.forEach(col => {
|
|
633
|
+
html += '<div class="border-bottom" data-name="' + col.name + '">';
|
|
634
|
+
html += '<div class="px-4 py-2 hover-bg-light d-flex align-items-center gap-3 collection-item" ';
|
|
635
|
+
html += 'style="cursor: pointer; min-height: 48px;" ';
|
|
636
|
+
html += 'data-bs-target="#col-' + col.name + '">';
|
|
637
|
+
html += '<i class="fas fa-chevron-right text-muted transition-rotate" style="font-size: 10px;"></i>';
|
|
638
|
+
html += '<div class="d-flex flex-column">';
|
|
639
|
+
html += '<span class="fw-bold text-dark small">' + col.name + '</span>';
|
|
640
|
+
html += '<span class="text-muted x-small opacity-75 fw-normal">' + col.fields.length + ' fields</span>';
|
|
641
|
+
html += '</div>';
|
|
642
|
+
html += '<button class="btn btn-sm btn-link ms-auto p-1 text-primary opacity-50 hover-opacity-100 stop-prop">';
|
|
643
|
+
html += '<i class="fas fa-plus"></i>';
|
|
644
|
+
html += '</button>';
|
|
645
|
+
html += '</div>';
|
|
646
|
+
html += '<div class="collapse" id="col-' + col.name + '">';
|
|
647
|
+
html += '<div class="px-4 pb-3 pt-2 d-flex flex-column gap-1" style="margin-left: 36px; border-left: 2px solid #f1f5f9;">';
|
|
648
|
+
col.fields.forEach(f => {
|
|
649
|
+
html += '<div class="x-small text-dark fw-bold py-1 d-flex align-items-center justify-content-between group-hover-visible">';
|
|
650
|
+
html += '<div class="d-flex align-items-center gap-2"><i class="fas fa-hashtag opacity-25" style="font-size: 10px;"></i>' + f + '</div>';
|
|
651
|
+
html += '<button class="btn btn-link btn-sm p-0 opacity-0 field-add-btn text-primary" data-field="' + f + '"><i class="fas fa-plus-circle"></i></button>';
|
|
652
|
+
html += '</div>';
|
|
653
|
+
});
|
|
654
|
+
html += '</div>';
|
|
655
|
+
html += '</div>';
|
|
656
|
+
html += '</div>';
|
|
657
|
+
});
|
|
658
|
+
schemaListEl.innerHTML = html;
|
|
659
|
+
|
|
660
|
+
const sSearch = document.getElementById('schema-search');
|
|
661
|
+
if (sSearch) {
|
|
662
|
+
sSearch.oninput = (e) => {
|
|
663
|
+
const q = (e.target).value.toLowerCase();
|
|
664
|
+
const items = schemaListEl.querySelectorAll('.border-bottom[data-name]');
|
|
665
|
+
items.forEach(item => {
|
|
666
|
+
if (item.getAttribute('data-name').includes(q)) {
|
|
667
|
+
item.classList.remove('d-none');
|
|
668
|
+
} else {
|
|
669
|
+
item.classList.add('d-none');
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
document.querySelectorAll('.collection-item').forEach(item => {
|
|
676
|
+
(item).onclick = (e) => {
|
|
677
|
+
const name = item.getAttribute('data-name');
|
|
678
|
+
if (name) (window).showCollectionDocs(name);
|
|
679
|
+
const targetId = item.getAttribute('data-bs-target');
|
|
680
|
+
const targetEl = targetId ? document.querySelector(targetId) : null;
|
|
681
|
+
if (targetEl) {
|
|
682
|
+
const isShowing = targetEl.classList.contains('show');
|
|
683
|
+
const bsCollapse = new (window).bootstrap.Collapse(targetEl, { toggle: false });
|
|
684
|
+
if (isShowing) bsCollapse.hide(); else bsCollapse.show();
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const expandAll = document.getElementById('expand-all');
|
|
690
|
+
const collapseAll = document.getElementById('collapse-all');
|
|
691
|
+
if (expandAll) {
|
|
692
|
+
expandAll.onclick = () => {
|
|
693
|
+
document.querySelectorAll('.collapse').forEach(el => {
|
|
694
|
+
try { new (window).bootstrap.Collapse(el, { toggle: false }).show(); } catch(e){}
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (collapseAll) {
|
|
699
|
+
collapseAll.onclick = () => {
|
|
700
|
+
document.querySelectorAll('.collapse').forEach(el => {
|
|
701
|
+
try { new (window).bootstrap.Collapse(el, { toggle: false }).hide(); } catch(e){}
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
} catch (e) {
|
|
706
|
+
console.error('Schema fetch error:', e);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
async function execute() {
|
|
711
|
+
lookUpElements();
|
|
712
|
+
if (!editor || !results) return;
|
|
713
|
+
const query = editor.value.trim();
|
|
714
|
+
if (!query) return;
|
|
715
|
+
localStorage.setItem('qq_console_query', query);
|
|
716
|
+
if (runBtn) runBtn.disabled = true;
|
|
717
|
+
results.style.opacity = '0.5';
|
|
718
|
+
|
|
719
|
+
// Grab custom headers from UI
|
|
720
|
+
const customHeaders = {};
|
|
721
|
+
if (headerList) {
|
|
722
|
+
headerList.querySelectorAll('.header-row').forEach(row => {
|
|
723
|
+
const kInput = row.querySelector('.h-key');
|
|
724
|
+
const vInput = row.querySelector('.h-value');
|
|
725
|
+
if (kInput && vInput) {
|
|
726
|
+
const k = kInput.value.trim();
|
|
727
|
+
const v = vInput.value.trim();
|
|
728
|
+
if (k && v) customHeaders[k] = v;
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const startTime = performance.now();
|
|
734
|
+
const payload = { query };
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
if (httpMeta) httpMeta.innerText = JSON.stringify(payload, null, 2);
|
|
738
|
+
|
|
739
|
+
const res = await fetch(config.endpoint, {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, customHeaders),
|
|
742
|
+
body: JSON.stringify(payload)
|
|
743
|
+
});
|
|
744
|
+
const data = await res.json();
|
|
745
|
+
const duration = Math.round(performance.now() - startTime);
|
|
746
|
+
|
|
747
|
+
if (httpStatus) {
|
|
748
|
+
const isErr = res.status >= 400;
|
|
749
|
+
httpStatus.innerHTML = '<span class="' + (isErr ? 'text-danger' : 'text-success') + ' fw-bold">' + res.status + ' ' + res.statusText + '</span>';
|
|
750
|
+
}
|
|
751
|
+
if (httpTime) httpTime.innerText = duration + 'ms';
|
|
752
|
+
if (httpHeaders) {
|
|
753
|
+
let hStr = '';
|
|
754
|
+
try {
|
|
755
|
+
for (let [k, v] of res.headers.entries()) hStr += '<b>' + k + '</b>: <span class="text-muted">' + v + '</span><br>';
|
|
756
|
+
} catch(e) {}
|
|
757
|
+
httpHeaders.innerHTML = hStr || '--';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
results.innerHTML = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
761
|
+
const state = window.playgroundState;
|
|
762
|
+
const currentTab = state.tabs.find(t => t.id === state.activeTabId);
|
|
763
|
+
if (currentTab) currentTab.results = results.innerHTML;
|
|
764
|
+
|
|
765
|
+
saveHistory({ payload, data, status: res.status, statusText: res.statusText, duration, headers: Object.fromEntries(res.headers.entries()) });
|
|
766
|
+
if (perfTimer) perfTimer.innerText = duration + 'ms';
|
|
767
|
+
} catch (e) {
|
|
768
|
+
results.innerHTML = 'Error: ' + e.message;
|
|
769
|
+
saveHistory({ payload: { query: editor.value }, data: { error: e.message }, status: 0, statusText: 'Fetch Error', duration: 0, headers: {} });
|
|
770
|
+
if (httpStatus) httpStatus.innerHTML = '<span class="text-danger fw-bold">ERROR</span>';
|
|
771
|
+
if (httpTime) httpTime.innerText = '--';
|
|
772
|
+
if (httpHeaders) httpHeaders.innerHTML = '--';
|
|
773
|
+
} finally {
|
|
774
|
+
if (runBtn) runBtn.disabled = false;
|
|
775
|
+
results.style.opacity = '1';
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function saveHistory(item) {
|
|
780
|
+
const hist = (window).playgroundHistory;
|
|
781
|
+
hist.unshift({ ...item, id: Date.now(), timestamp: new Date().toLocaleTimeString() });
|
|
782
|
+
if (hist.length > 25) hist.pop();
|
|
783
|
+
localStorage.setItem('qq_history', JSON.stringify(hist));
|
|
784
|
+
renderHistory();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function renderHistory() {
|
|
788
|
+
const hList = document.getElementById('history-list');
|
|
789
|
+
if (!hList) return;
|
|
790
|
+
const hist = (window).playgroundHistory;
|
|
791
|
+
hList.innerHTML = hist.map((h, i) => \`
|
|
792
|
+
<div class="history-item p-2 mb-2 border rounded-3 bg-white hover-bg-light cursor-pointer shadow-sm \${i === 0 ? 'border-primary' : ''}" style="transition: all 0.2s;" onclick="window.restoreHistory(\${h.id})">
|
|
793
|
+
<div class="d-flex align-items-center justify-content-between mb-1">
|
|
794
|
+
<span class="x-small fw-bold \${h.status >= 400 || h.status === 0 ? 'text-danger' : 'text-success'}">\${h.status === 0 ? 'ERR' : h.status} \${h.statusText}</span>
|
|
795
|
+
<span class="x-small text-muted">\${h.timestamp}</span>
|
|
796
|
+
</div>
|
|
797
|
+
<div class="x-small text-truncate text-muted opacity-75 font-monospace">\${JSON.stringify(h.payload.query).substring(0, 40)}...</div>
|
|
798
|
+
<div class="text-end x-small text-muted mt-1 fw-bold opacity-50">\${h.duration}ms</div>
|
|
799
|
+
</div>
|
|
800
|
+
\`).join('') || '<div class="text-center p-3 opacity-25 x-small">No history</div>';
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
(window).restoreHistory = (id) => {
|
|
804
|
+
const hist = (window).playgroundHistory;
|
|
805
|
+
const item = hist.find(h => h.id === id);
|
|
806
|
+
if (!item) return;
|
|
807
|
+
|
|
808
|
+
if (results) results.innerHTML = JSON.stringify(item.data, null, 2);
|
|
809
|
+
if (httpStatus) httpStatus.innerHTML = '<span class="' + (item.status >= 400 || item.status === 0 ? 'text-danger' : 'text-success') + ' fw-bold">' + item.status + ' ' + item.statusText + '</span>';
|
|
810
|
+
if (httpTime) httpTime.innerText = item.duration + 'ms';
|
|
811
|
+
if (httpMeta) httpMeta.innerText = JSON.stringify(item.payload, null, 2);
|
|
812
|
+
if (httpHeaders) {
|
|
813
|
+
let hStr = '';
|
|
814
|
+
for (let k in item.headers) hStr += '<b>' + k + '</b>: <span class="text-muted">' + item.headers[k] + '</span><br>';
|
|
815
|
+
httpHeaders.innerHTML = hStr || '--';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Highlight selected in sidebar
|
|
819
|
+
document.querySelectorAll('.history-item').forEach(el => el.classList.remove('border-primary'));
|
|
820
|
+
const active = Array.from(document.querySelectorAll('.history-item')).find(el => el.getAttribute('onclick')?.includes(id));
|
|
821
|
+
if (active) active.classList.add('border-primary');
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// Visualizer Logic
|
|
825
|
+
(window).visNodes = [];
|
|
826
|
+
(window).visOffset = { x: 0, y: 0 };
|
|
827
|
+
(window).dragNode = null;
|
|
828
|
+
(window).dragOffset = { x: 0, y: 0 };
|
|
829
|
+
|
|
830
|
+
(window).initVisualizer = async () => {
|
|
831
|
+
if(!(window).visInitialized) {
|
|
832
|
+
const res = await fetch(config.endpoint + '?schema=true');
|
|
833
|
+
const data = await res.json();
|
|
834
|
+
(window).visNodes = data.map((s, i) => ({
|
|
835
|
+
name: s.name, x: 200 + (i % 2) * 450, y: 200 + Math.floor(i / 2) * 400,
|
|
836
|
+
w: 280, h: 80 + s.fields.length * 26 + (s.relations?.length || 0) * 26, fields: s.fields, relations: s.relations
|
|
837
|
+
}));
|
|
838
|
+
|
|
839
|
+
// Restore drag and pan positions from cache
|
|
840
|
+
const savedPositions = localStorage.getItem('quickql_vis_positions');
|
|
841
|
+
if (savedPositions) {
|
|
842
|
+
try {
|
|
843
|
+
const pos = JSON.parse(savedPositions);
|
|
844
|
+
(window).visNodes.forEach(node => {
|
|
845
|
+
if (pos[node.name]) {
|
|
846
|
+
node.x = pos[node.name].x;
|
|
847
|
+
node.y = pos[node.name].y;
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
} catch(e) {}
|
|
851
|
+
}
|
|
852
|
+
const savedOffset = localStorage.getItem('quickql_vis_offset');
|
|
853
|
+
if (savedOffset) {
|
|
854
|
+
try {
|
|
855
|
+
(window).visOffset = JSON.parse(savedOffset);
|
|
856
|
+
} catch(e) {}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
(window).visInitialized = true;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const canvas = document.getElementById('visualizer-canvas');
|
|
863
|
+
if (canvas) {
|
|
864
|
+
canvas.onmousedown = (e) => {
|
|
865
|
+
const rect = canvas.getBoundingClientRect();
|
|
866
|
+
const mouseX = e.clientX - rect.left - (window).visOffset.x;
|
|
867
|
+
const mouseY = e.clientY - rect.top - (window).visOffset.y;
|
|
868
|
+
|
|
869
|
+
let foundNode = false;
|
|
870
|
+
for (let i = ((window).visNodes || []).length - 1; i >= 0; i--) {
|
|
871
|
+
const n = (window).visNodes[i];
|
|
872
|
+
if (mouseX >= n.x && mouseX <= n.x + n.w && mouseY >= n.y && mouseY <= n.y + n.h) {
|
|
873
|
+
(window).dragNode = n;
|
|
874
|
+
(window).dragOffset = { x: mouseX - n.x, y: mouseY - n.y };
|
|
875
|
+
canvas.style.cursor = 'grabbing';
|
|
876
|
+
foundNode = true;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!foundNode) {
|
|
882
|
+
(window).isPanning = true;
|
|
883
|
+
(window).panStart = { x: e.clientX, y: e.clientY };
|
|
884
|
+
canvas.style.cursor = 'all-scroll';
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
canvas.onmousemove = (e) => {
|
|
889
|
+
if ((window).dragNode) {
|
|
890
|
+
const rect = canvas.getBoundingClientRect();
|
|
891
|
+
const mouseX = e.clientX - rect.left - (window).visOffset.x;
|
|
892
|
+
const mouseY = e.clientY - rect.top - (window).visOffset.y;
|
|
893
|
+
|
|
894
|
+
let newX = mouseX - (window).dragOffset.x;
|
|
895
|
+
let newY = mouseY - (window).dragOffset.y;
|
|
896
|
+
|
|
897
|
+
(window).dragNode.x = newX;
|
|
898
|
+
(window).dragNode.y = newY;
|
|
899
|
+
(window).drawVis();
|
|
900
|
+
} else if ((window).isPanning) {
|
|
901
|
+
const dx = e.clientX - (window).panStart.x;
|
|
902
|
+
const dy = e.clientY - (window).panStart.y;
|
|
903
|
+
(window).visOffset.x += dx;
|
|
904
|
+
(window).visOffset.y += dy;
|
|
905
|
+
(window).panStart = { x: e.clientX, y: e.clientY };
|
|
906
|
+
(window).drawVis();
|
|
907
|
+
} else {
|
|
908
|
+
const rect = canvas.getBoundingClientRect();
|
|
909
|
+
const mouseX = e.clientX - rect.left - (window).visOffset.x;
|
|
910
|
+
const mouseY = e.clientY - rect.top - (window).visOffset.y;
|
|
911
|
+
let hovering = false;
|
|
912
|
+
for (let i = 0; i < ((window).visNodes || []).length; i++) {
|
|
913
|
+
const n = (window).visNodes[i];
|
|
914
|
+
if (mouseX >= n.x && mouseX <= n.x + n.w && mouseY >= n.y && mouseY <= n.y + n.h) {
|
|
915
|
+
hovering = true;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
canvas.style.cursor = hovering ? 'grab' : 'default';
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
canvas.onmouseup = () => {
|
|
924
|
+
(window).dragNode = null;
|
|
925
|
+
(window).isPanning = false;
|
|
926
|
+
if (canvas) canvas.style.cursor = 'default';
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
canvas.onmouseleave = () => {
|
|
930
|
+
(window).dragNode = null;
|
|
931
|
+
(window).isPanning = false;
|
|
932
|
+
if (canvas) canvas.style.cursor = 'default';
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
(window).drawVis();
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
(window).drawVis = () => {
|
|
940
|
+
const visualizerCanvas = document.getElementById('visualizer-canvas');
|
|
941
|
+
const visualizerCtx = visualizerCanvas ? visualizerCanvas.getContext('2d') : null;
|
|
942
|
+
if (!visualizerCanvas || !visualizerCtx) return;
|
|
943
|
+
visualizerCanvas.width = visualizerCanvas.parentElement.clientWidth;
|
|
944
|
+
visualizerCanvas.height = visualizerCanvas.parentElement.clientHeight;
|
|
945
|
+
visualizerCtx.clearRect(0, 0, visualizerCanvas.width, visualizerCanvas.height);
|
|
946
|
+
|
|
947
|
+
// Save bounds real-time internally to storage
|
|
948
|
+
if ((window).visNodes && (window).visNodes.length > 0 && (window).visInitialized) {
|
|
949
|
+
const positions = {};
|
|
950
|
+
(window).visNodes.forEach(node => {
|
|
951
|
+
positions[node.name] = { x: node.x, y: node.y };
|
|
952
|
+
});
|
|
953
|
+
localStorage.setItem('quickql_vis_positions', JSON.stringify(positions));
|
|
954
|
+
localStorage.setItem('quickql_vis_offset', JSON.stringify((window).visOffset));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Draw Dots Grid
|
|
958
|
+
visualizerCtx.fillStyle = '#cbd5e1';
|
|
959
|
+
const dotSpacing = 20;
|
|
960
|
+
const offsetX = ((window).visOffset.x % dotSpacing + dotSpacing) % dotSpacing;
|
|
961
|
+
const offsetY = ((window).visOffset.y % dotSpacing + dotSpacing) % dotSpacing;
|
|
962
|
+
for (let x = offsetX; x < visualizerCanvas.width; x += dotSpacing) {
|
|
963
|
+
for (let y = offsetY; y < visualizerCanvas.height; y += dotSpacing) {
|
|
964
|
+
visualizerCtx.fillRect(x, y, 2, 2);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
visualizerCtx.save();
|
|
969
|
+
visualizerCtx.translate((window).visOffset.x, (window).visOffset.y);
|
|
970
|
+
|
|
971
|
+
// Draw Relation Wires first (behind nodes)
|
|
972
|
+
((window).visNodes || []).forEach(node => {
|
|
973
|
+
if (node.relations) {
|
|
974
|
+
node.relations.forEach((rel, ri) => {
|
|
975
|
+
const targetNode = (window).visNodes.find(n => n.name === rel.target);
|
|
976
|
+
if (targetNode) {
|
|
977
|
+
visualizerCtx.beginPath();
|
|
978
|
+
|
|
979
|
+
let startX, endX, cp1x, cp2x;
|
|
980
|
+
const startY = node.y + 80 + ((node.fields?.length || 0) + ri) * 26 + 10; // mid of relation label
|
|
981
|
+
const endY = targetNode.y + 32; // mid of target header
|
|
982
|
+
|
|
983
|
+
let arrowPointLeft = false;
|
|
984
|
+
|
|
985
|
+
if (node.x + node.w / 2 < targetNode.x + targetNode.w / 2) {
|
|
986
|
+
// Origin node is to the left of Target node
|
|
987
|
+
startX = node.x + node.w; // Start from right edge
|
|
988
|
+
endX = targetNode.x - 4; // End at left edge
|
|
989
|
+
cp1x = startX + 100;
|
|
990
|
+
cp2x = endX - 100;
|
|
991
|
+
arrowPointLeft = false;
|
|
992
|
+
} else {
|
|
993
|
+
// Origin node is to the right of Target node
|
|
994
|
+
startX = node.x; // Start from left edge
|
|
995
|
+
endX = targetNode.x + targetNode.w + 4; // End at right edge
|
|
996
|
+
cp1x = startX - 100;
|
|
997
|
+
cp2x = endX + 100;
|
|
998
|
+
arrowPointLeft = true;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
visualizerCtx.moveTo(startX, startY);
|
|
1002
|
+
// Bezier curve for wires
|
|
1003
|
+
visualizerCtx.bezierCurveTo(cp1x, startY, cp2x, endY, endX, endY);
|
|
1004
|
+
visualizerCtx.strokeStyle = '#94a3b8';
|
|
1005
|
+
visualizerCtx.lineWidth = 2;
|
|
1006
|
+
visualizerCtx.stroke();
|
|
1007
|
+
|
|
1008
|
+
// Draw end arrowhead
|
|
1009
|
+
visualizerCtx.beginPath();
|
|
1010
|
+
visualizerCtx.moveTo(endX, endY);
|
|
1011
|
+
if (arrowPointLeft) {
|
|
1012
|
+
visualizerCtx.lineTo(endX + 10, endY - 6);
|
|
1013
|
+
visualizerCtx.lineTo(endX + 10, endY + 6);
|
|
1014
|
+
} else {
|
|
1015
|
+
visualizerCtx.lineTo(endX - 10, endY - 6);
|
|
1016
|
+
visualizerCtx.lineTo(endX - 10, endY + 6);
|
|
1017
|
+
}
|
|
1018
|
+
visualizerCtx.closePath();
|
|
1019
|
+
visualizerCtx.fillStyle = '#94a3b8';
|
|
1020
|
+
visualizerCtx.fill();
|
|
1021
|
+
|
|
1022
|
+
// Draw start arrowhead
|
|
1023
|
+
visualizerCtx.beginPath();
|
|
1024
|
+
visualizerCtx.moveTo(startX, startY);
|
|
1025
|
+
if (arrowPointLeft) {
|
|
1026
|
+
visualizerCtx.lineTo(startX - 10, startY - 6);
|
|
1027
|
+
visualizerCtx.lineTo(startX - 10, startY + 6);
|
|
1028
|
+
} else {
|
|
1029
|
+
visualizerCtx.lineTo(startX + 10, startY - 6);
|
|
1030
|
+
visualizerCtx.lineTo(startX + 10, startY + 6);
|
|
1031
|
+
}
|
|
1032
|
+
visualizerCtx.closePath();
|
|
1033
|
+
visualizerCtx.fillStyle = '#94a3b8';
|
|
1034
|
+
visualizerCtx.fill();
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
((window).visNodes || []).forEach(node => {
|
|
1041
|
+
// Node Box
|
|
1042
|
+
visualizerCtx.fillStyle = '#ffffff'; visualizerCtx.shadowBlur = 15; visualizerCtx.shadowColor = 'rgba(0,0,0,0.06)';
|
|
1043
|
+
visualizerCtx.beginPath();
|
|
1044
|
+
if (visualizerCtx.roundRect) {
|
|
1045
|
+
visualizerCtx.roundRect(node.x, node.y, node.w, node.h, 12);
|
|
1046
|
+
} else {
|
|
1047
|
+
visualizerCtx.rect(node.x, node.y, node.w, node.h);
|
|
1048
|
+
}
|
|
1049
|
+
visualizerCtx.fill();
|
|
1050
|
+
visualizerCtx.strokeStyle = '#e2e8f0'; visualizerCtx.lineWidth = 1; visualizerCtx.stroke();
|
|
1051
|
+
visualizerCtx.shadowColor = 'transparent';
|
|
1052
|
+
|
|
1053
|
+
// Node Title Header
|
|
1054
|
+
visualizerCtx.fillStyle = '#0f172a'; visualizerCtx.font = 'bold 14px sans-serif';
|
|
1055
|
+
visualizerCtx.fillText(node.name.toLowerCase(), node.x + 20, node.y + 32);
|
|
1056
|
+
|
|
1057
|
+
// Node Divider Line
|
|
1058
|
+
visualizerCtx.beginPath();
|
|
1059
|
+
visualizerCtx.moveTo(node.x, node.y + 50);
|
|
1060
|
+
visualizerCtx.lineTo(node.x + node.w, node.y + 50);
|
|
1061
|
+
visualizerCtx.strokeStyle = '#f1f5f9';
|
|
1062
|
+
visualizerCtx.stroke();
|
|
1063
|
+
|
|
1064
|
+
// Node Fields
|
|
1065
|
+
visualizerCtx.fillStyle = '#475569'; visualizerCtx.font = '13px monospace';
|
|
1066
|
+
(node.fields || []).forEach((f, fi) => {
|
|
1067
|
+
visualizerCtx.fillText(f, node.x + 20, node.y + 80 + (fi * 26));
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// Relation Labels inside Node Box (if any)
|
|
1071
|
+
if (node.relations) {
|
|
1072
|
+
node.relations.forEach((rel, ri) => {
|
|
1073
|
+
visualizerCtx.fillStyle = '#3b82f6'; visualizerCtx.font = 'bold 12px monospace';
|
|
1074
|
+
visualizerCtx.fillText(rel.name + ' → ' + rel.target, node.x + 20, node.y + 80 + ((node.fields?.length || 0) + ri) * 26);
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
visualizerCtx.restore();
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
window.addEventListener('hashchange', () => {
|
|
1082
|
+
const hash = window.location.hash ? window.location.hash.slice(1) : 'playground';
|
|
1083
|
+
if (validViews.includes(hash) && currentView !== hash) {
|
|
1084
|
+
(window).switchView(hash);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
(window).fetchSchema();
|
|
1089
|
+
const initialHash = window.location.hash ? window.location.hash.slice(1) : 'playground';
|
|
1090
|
+
const validViews = ['playground', 'schema', 'visualizer', 'docs'];
|
|
1091
|
+
(window).switchView(validViews.includes(initialHash) ? initialHash : 'playground');
|
|
1092
|
+
})();
|
|
1093
|
+
`;
|
|
1094
|
+
export const playgroundCss = `
|
|
1095
|
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono&display=swap');
|
|
1096
|
+
body { font-family: 'Plus Jakarta Sans', sans-serif; background: #f8fafc; margin: 0; }
|
|
1097
|
+
.font-monospace { font-family: 'JetBrains Mono', monospace !important; }
|
|
1098
|
+
.nav-view-btn.active { color: #0d6efd !important; background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
|
1099
|
+
.hover-bg-light:hover { background: #f8fafc; }
|
|
1100
|
+
.transition-rotate { transition: transform 0.2s; }
|
|
1101
|
+
#editor { outline: none !important; box-shadow: none !important; caret-color: #0d6efd; line-height: 1.6; }
|
|
1102
|
+
#editor:focus { outline: none; box-shadow: none; }
|
|
1103
|
+
#gutter { overflow: hidden; line-height: 1.6; }
|
|
1104
|
+
.x-small { font-size: 11px !important; }
|
|
1105
|
+
pre { tab-size: 2; }
|
|
1106
|
+
@media (max-width: 1200px) {
|
|
1107
|
+
.nav-btn-text { display: none; }
|
|
1108
|
+
#perf-timer { display: none; }
|
|
1109
|
+
.vr { display: none; }
|
|
1110
|
+
#status-chip { display: none; }
|
|
1111
|
+
}
|
|
1112
|
+
`;
|
|
1113
|
+
//# sourceMappingURL=assets.js.map
|