@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL Lexer (Tokenizer)
|
|
3
|
+
*
|
|
4
|
+
* Scans raw QuickQL string input and produces a stream of tokens
|
|
5
|
+
* for the syntax parser.
|
|
6
|
+
*
|
|
7
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
8
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
export class Tokenizer {
|
|
12
|
+
input;
|
|
13
|
+
cursor = 0;
|
|
14
|
+
constructor(input) {
|
|
15
|
+
this.input = input;
|
|
16
|
+
}
|
|
17
|
+
tokenize() {
|
|
18
|
+
const tokens = [];
|
|
19
|
+
while (this.cursor < this.input.length) {
|
|
20
|
+
const char = this.input[this.cursor];
|
|
21
|
+
if (/\s/.test(char) || char === ',') {
|
|
22
|
+
this.cursor++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (char === '{') {
|
|
26
|
+
tokens.push({ type: 'LBRACE', value: '{' });
|
|
27
|
+
this.cursor++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === '}') {
|
|
31
|
+
tokens.push({ type: 'RBRACE', value: '}' });
|
|
32
|
+
this.cursor++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === '(') {
|
|
36
|
+
tokens.push({ type: 'LPAREN', value: '(' });
|
|
37
|
+
this.cursor++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === ')') {
|
|
41
|
+
tokens.push({ type: 'RPAREN', value: ')' });
|
|
42
|
+
this.cursor++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === ':') {
|
|
46
|
+
tokens.push({ type: 'COLON', value: ':' });
|
|
47
|
+
this.cursor++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === '*') {
|
|
51
|
+
tokens.push({ type: 'STAR', value: '*' });
|
|
52
|
+
this.cursor++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Strings (quotes)
|
|
56
|
+
if (char === '"' || char === "'") {
|
|
57
|
+
const quote = char;
|
|
58
|
+
let value = '';
|
|
59
|
+
this.cursor++;
|
|
60
|
+
while (this.cursor < this.input.length && this.input[this.cursor] !== quote) {
|
|
61
|
+
value += this.input[this.cursor];
|
|
62
|
+
this.cursor++;
|
|
63
|
+
}
|
|
64
|
+
this.cursor++;
|
|
65
|
+
tokens.push({ type: 'STRING', value });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Numbers
|
|
69
|
+
if (/\d/.test(char)) {
|
|
70
|
+
let value = '';
|
|
71
|
+
while (this.cursor < this.input.length && /\d/.test(this.input[this.cursor])) {
|
|
72
|
+
value += this.input[this.cursor];
|
|
73
|
+
this.cursor++;
|
|
74
|
+
}
|
|
75
|
+
tokens.push({ type: 'NUMBER', value });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Names (fields, root query, arguments)
|
|
79
|
+
if (/[a-zA-Z_]/.test(char)) {
|
|
80
|
+
let value = '';
|
|
81
|
+
while (this.cursor < this.input.length && /[a-zA-Z0-9_]/.test(this.input[this.cursor])) {
|
|
82
|
+
value += this.input[this.cursor];
|
|
83
|
+
this.cursor++;
|
|
84
|
+
}
|
|
85
|
+
tokens.push({ type: 'NAME', value });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Unexpected character: ${char} at position ${this.cursor}`);
|
|
89
|
+
}
|
|
90
|
+
tokens.push({ type: 'EOF', value: '' });
|
|
91
|
+
return tokens;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=tokenizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../../../../src/parser/tokenizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,OAAO,SAAS;IACZ,KAAK,CAAS;IACd,MAAM,GAAG,CAAC,CAAC;IAEnB,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,QAAQ;QACN,MAAM,MAAM,GAAY,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAErC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACpC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YAED,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YAED,mBAAmB;YACnB,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjC,MAAM,KAAK,GAAG,IAAI,CAAC;gBACnB,IAAI,KAAK,GAAG,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,OAAO,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC;oBAC5E,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBACjC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,CAAC;gBACD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBACvC,SAAS;YACX,CAAC;YAED,UAAU;YACV,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpB,IAAI,KAAK,GAAG,EAAE,CAAC;gBACf,OAAO,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;oBAC7E,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBACjC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBACvC,SAAS;YACX,CAAC;YAED,wCAAwC;YACxC,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,IAAI,KAAK,GAAG,EAAE,CAAC;gBACf,OAAO,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;oBACvF,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBACjC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBACrC,SAAS;YACX,CAAC;YAED,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,gBAAgB,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACxC,OAAO,MAAM,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL AST Transformer
|
|
3
|
+
*
|
|
4
|
+
* Converts the Abstract Syntax Tree (AST) generated by the parser into
|
|
5
|
+
* standard QuickQLRequest and Query objects.
|
|
6
|
+
*
|
|
7
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
8
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
import { Query, QuickQLRequest } from '@quickql/core';
|
|
12
|
+
import { ASTNode } from './types';
|
|
13
|
+
export declare class Transformer {
|
|
14
|
+
/**
|
|
15
|
+
* Transforms the top-level AST into a standard QuickQLRequest structure.
|
|
16
|
+
*/
|
|
17
|
+
static transformSchema(ast: Record<string, ASTNode>): QuickQLRequest;
|
|
18
|
+
/**
|
|
19
|
+
* Recursive transformation from syntax node to internal Query object.
|
|
20
|
+
* Ensures identical parity between JSON and String queries.
|
|
21
|
+
*/
|
|
22
|
+
static transformNode(node: ASTNode): Query;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=transformer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../../../../src/parser/transformer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,qBAAa,WAAW;IACtB;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc;IAUpE;;;OAGG;IACH,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,KAAK;CAwC3C"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL AST Transformer
|
|
3
|
+
*
|
|
4
|
+
* Converts the Abstract Syntax Tree (AST) generated by the parser into
|
|
5
|
+
* standard QuickQLRequest and Query objects.
|
|
6
|
+
*
|
|
7
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
8
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
export class Transformer {
|
|
12
|
+
/**
|
|
13
|
+
* Transforms the top-level AST into a standard QuickQLRequest structure.
|
|
14
|
+
*/
|
|
15
|
+
static transformSchema(ast) {
|
|
16
|
+
const queries = {};
|
|
17
|
+
for (const [key, node] of Object.entries(ast)) {
|
|
18
|
+
queries[key] = this.transformNode(node);
|
|
19
|
+
}
|
|
20
|
+
return { queries };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Recursive transformation from syntax node to internal Query object.
|
|
24
|
+
* Ensures identical parity between JSON and String queries.
|
|
25
|
+
*/
|
|
26
|
+
static transformNode(node) {
|
|
27
|
+
const query = {
|
|
28
|
+
from: node.name
|
|
29
|
+
};
|
|
30
|
+
// If an alias was provided in the syntax (alias: name), we capture it.
|
|
31
|
+
if (node.alias) {
|
|
32
|
+
query.alias = node.alias;
|
|
33
|
+
}
|
|
34
|
+
// Arguments are directly mapped to the internal 'where' clause.
|
|
35
|
+
if (node.arguments && Object.keys(node.arguments).length > 0) {
|
|
36
|
+
query.where = node.arguments;
|
|
37
|
+
}
|
|
38
|
+
// Process field selection set.
|
|
39
|
+
if (node.fields && node.fields.length > 0) {
|
|
40
|
+
const select = [];
|
|
41
|
+
const include = {};
|
|
42
|
+
for (const field of node.fields) {
|
|
43
|
+
if (typeof field === 'string') {
|
|
44
|
+
select.push(field);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Nested relations represent a standard QuickQL 'include' operation.
|
|
48
|
+
include[field.name] = this.transformNode(field);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (select.length > 0) {
|
|
52
|
+
query.select = select;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(include).length > 0) {
|
|
55
|
+
query.include = include;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return query;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=transformer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.js","sourceRoot":"","sources":["../../../../src/parser/transformer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,OAAO,WAAW;IACtB;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,GAA4B;QACjD,MAAM,OAAO,GAA0B,EAAE,CAAC;QAE1C,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9C,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,aAAa,CAAC,IAAa;QAChC,MAAM,KAAK,GAAU;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC;QAEF,uEAAuE;QACvE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAC3B,CAAC;QAED,gEAAgE;QAChE,IAAI,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7D,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/B,CAAC;QAED,+BAA+B;QAC/B,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,MAAM,OAAO,GAA0B,EAAE,CAAC;YAE1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrB,CAAC;qBAAM,CAAC;oBACN,qEAAqE;oBACrE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;YAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;YACxB,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;YAC1B,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL Parser Types
|
|
3
|
+
*
|
|
4
|
+
* Token and AST (Abstract Syntax Tree) definitions for the syntax engine.
|
|
5
|
+
*
|
|
6
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
7
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
8
|
+
* License: MIT
|
|
9
|
+
*/
|
|
10
|
+
export type TokenType = 'NAME' | 'COLON' | 'LBRACE' | 'RBRACE' | 'LPAREN' | 'RPAREN' | 'STRING' | 'NUMBER' | 'COMMA' | 'STAR' | 'EOF';
|
|
11
|
+
export interface Token {
|
|
12
|
+
type: TokenType;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ASTNode {
|
|
16
|
+
name: string;
|
|
17
|
+
alias?: string;
|
|
18
|
+
arguments?: Record<string, any>;
|
|
19
|
+
fields?: (string | ASTNode)[];
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/parser/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,MAAM,GACN,KAAK,CAAC;AAEV,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;CAC/B"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuickQL Parser Types
|
|
3
|
+
*
|
|
4
|
+
* Token and AST (Abstract Syntax Tree) definitions for the syntax engine.
|
|
5
|
+
*
|
|
6
|
+
* (c) 2024-2026 Udinmo Inc. All rights reserved.
|
|
7
|
+
* Author: Udinmo Inc. <engineering@udinmo.com>
|
|
8
|
+
* License: MIT
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/parser/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
/**
|
|
12
|
+
* SUPREME LIGHT Logic for the QuickQL Console.
|
|
13
|
+
*/
|
|
14
|
+
export declare const playgroundJs = "\n\nfunction renderDocsPage() {\n const schema = (window).currentSchema || [];\n if (schema.length === 0) {\n return '<div class=\"p-5 text-center text-muted\"><div class=\"mb-3\" style=\"font-size: 48px; opacity: 0.15;\"><i class=\"fas fa-book-open\"></i></div><h4 class=\"fw-bold\">No Schema Loaded</h4><p class=\"text-muted\">Navigate to a collection or refresh to load schema metadata.</p></div>';\n }\n\n // --- Helper: infer type badge ---\n function getTypeInfo(fName) {\n if (fName === 'id' || fName.endsWith('_id')) return { label: 'ID', color: '#7c3aed', bg: '#ede9fe' };\n if (fName === 'email') return { label: 'Email', color: '#0891b2', bg: '#e0f2fe' };\n if (fName === 'created_at' || fName === 'updated_at' || fName.endsWith('_at')) return { label: 'DateTime', color: '#d97706', bg: '#fef3c7' };\n if (fName.startsWith('is_') || fName.startsWith('has_') || fName === 'active' || fName === 'enabled') return { label: 'Boolean', color: '#059669', bg: '#d1fae5' };\n if (fName.includes('count') || fName === 'age' || fName === 'total' || fName.includes('amount') || fName.includes('price')) return { label: 'Number', color: '#dc2626', bg: '#fee2e2' };\n if (fName === 'password' || fName === 'token' || fName === 'secret') return { label: 'Hash', color: '#64748b', bg: '#f1f5f9' };\n if (fName.includes('url') || fName.includes('image') || fName.includes('avatar') || fName.includes('link')) return { label: 'URL', color: '#6366f1', bg: '#eef2ff' };\n return { label: 'String', color: '#475569', bg: '#f8fafc' };\n }\n\n function getFieldDesc(fName, colName) {\n if (fName === 'id') return 'Primary key. Unique record identifier \u2014 used for targeted lookups and as a foreign key reference in relations.';\n if (fName.endsWith('_id')) return 'Foreign key reference to a related <strong>' + fName.replace('_id','') + '</strong> record.';\n if (fName === 'email') return 'Email address. Used for authentication and contact. Must be a valid RFC 5321 format.';\n if (fName.includes('name')) return 'Human-readable display name or label for the <strong>' + colName + '</strong> record.';\n if (fName === 'created_at') return 'Timestamp of when this record was first persisted. Set automatically by the system.';\n if (fName === 'updated_at') return 'Timestamp of the last modification. Automatically updated on every write operation.';\n if (fName === 'password') return 'Hashed credential string. Never returned in plain text \u2014 always stored using a one-way hash.';\n if (fName.startsWith('is_')) return 'Boolean flag. Indicates whether the <em>' + fName.replace('is_','') + '</em> condition is true for this record.';\n if (fName.startsWith('has_')) return 'Boolean flag. Indicates presence or availability of <em>' + fName.replace('has_','') + '</em>.';\n if (fName.includes('count')) return 'Aggregate counter. Denormalized value tracking the number of related sub-items.';\n if (fName.includes('url') || fName.includes('image') || fName.includes('avatar')) return 'URL pointing to an external or stored resource. Should be a fully qualified URI.';\n if (fName.includes('amount') || fName.includes('price') || fName.includes('total')) return 'Monetary or numeric value. Stored as a fixed-precision number to avoid floating-point errors.';\n return 'Data attribute on <strong>' + colName + '</strong>. Stores domain-specific information for this field.';\n }\n\n let html = '';\n html += '<div class=\"d-flex h-100 bg-white\" style=\"min-width: 0;\">';\n\n // --- Left nav sidebar ---\n html += '<aside class=\"flex-shrink-0 border-end d-flex flex-column\" style=\"width: 190px; overflow-y: auto; background: #fafafa;\">';\n html += ' <div class=\"px-4 pt-4 pb-3 border-bottom bg-white\">';\n html += ' <div class=\"text-uppercase fw-bold text-muted mb-1\" style=\"font-size: 10px; letter-spacing: 2px;\">Collections</div>';\n html += ' <div class=\"text-muted\" style=\"font-size: 11px;\">' + schema.length + ' collection' + (schema.length !== 1 ? 's' : '') + ' detected</div>';\n html += ' </div>';\n html += ' <div class=\"py-2\">';\n schema.forEach((col, idx) => {\n const colors = ['#3b82f6','#8b5cf6','#ec4899','#10b981','#f59e0b','#ef4444','#06b6d4'];\n const c = colors[idx % colors.length];\n html += '<a href=\"#doc-' + col.name + '\" class=\"d-flex align-items-center gap-3 px-4 py-2 text-decoration-none docs-nav-item\" style=\"color: #374151; font-size: 13px; font-weight: 600; transition: background 0.15s;\">';\n html += ' <span style=\"width: 8px; height: 8px; border-radius: 50%; background: ' + c + '; flex-shrink: 0; display: inline-block;\"></span>';\n html += ' <span>' + col.name + '</span>';\n html += ' <span class=\"ms-auto text-muted fw-normal\" style=\"font-size: 11px;\">' + col.fields.length + 'f</span>';\n html += '</a>';\n });\n html += ' </div>';\n html += '</aside>';\n\n // --- Main docs body ---\n html += '<main class=\"flex-grow-1 overflow-auto\" style=\"min-width: 0; background: #fff;\">';\n html += ' <div class=\"mx-auto\" style=\"max-width: 900px; padding: 28px 24px;\">';\n\n // Header\n html += ' <div class=\"mb-4 pb-3 border-bottom\">';\n html += ' <div class=\"d-flex align-items-start justify-content-between\">';\n html += ' <div>';\n html += ' <h1 class=\"fw-bold mb-1\" style=\"font-size: 22px; letter-spacing: -0.5px;\">API Reference</h1>';\n html += ' <p class=\"text-muted mb-0\" style=\"font-size: 13px;\">Auto-generated docs for your QuickQL schema. Uses the QuickQL native query language.</p>';\n html += ' </div>';\n html += ' <span class=\"badge px-3 py-2 ms-3 flex-shrink-0\" style=\"background: #d1fae5; color: #065f46; font-size: 10px; border-radius: 100px; font-weight: 700;\">\u25CF LIVE</span>';\n html += ' </div>';\n html += ' </div>';\n\n // Query Syntax intro box\n html += ' <div class=\"mb-4 p-3 rounded-2\" style=\"background: #f8fafc; border: 1px solid #e2e8f0;\">';\n html += ' <div class=\"d-flex align-items-center gap-2 mb-2\">';\n html += ' <i class=\"fas fa-terminal text-primary\" style=\"font-size: 11px;\"></i>';\n html += ' <span class=\"fw-bold text-dark\" style=\"font-size: 11px; text-transform: uppercase; letter-spacing: 1px;\">QuickQL Query Syntax</span>';\n html += ' </div>';\n html += ' <div style=\"display: flex; gap: 12px; flex-wrap: wrap;\">';\n html += ' <div class=\"mb-4\" style=\"flex: 1 1 100%; min-width: 0;\">';\n html += ' <pre class=\"rounded-2 p-3 mb-1 font-monospace\" style=\"background: #0f172a; color: #e2e8f0; font-size: 12px; line-height: 1.7; overflow-x: auto; white-space: pre; margin: 0;\">users {\\n id,\\n username,\\n email\\n}</pre>';\n html += ' <div class=\"text-muted mt-1\" style=\"font-size: 11px;\"><i class=\"fas fa-arrow-right me-1\"></i>Select fields by name, comma-separated</div>';\n html += ' </div>';\n html += ' <div style=\"flex: 1 1 100%; min-width: 0;\">';\n html += ' <pre class=\"rounded-2 p-3 mb-1 font-monospace\" style=\"background: #0f172a; color: #e2e8f0; font-size: 12px; line-height: 1.7; overflow-x: auto; white-space: pre; margin: 0;\">users {\\n id,\\n posts { id, title }\\n}</pre>';\n html += ' <div class=\"text-muted mt-1\" style=\"font-size: 11px;\"><i class=\"fas fa-arrow-right me-1\"></i>Nest relation names inline for joins</div>';\n html += ' </div>';\n html += ' </div>';\n html += ' </div>';\n\n // Per-collection docs\n schema.forEach((col, idx) => {\n const colors = ['#3b82f6','#8b5cf6','#ec4899','#10b981','#f59e0b','#ef4444','#06b6d4'];\n const accentColor = colors[idx % colors.length];\n\n html += '<section id=\"doc-' + col.name + '\" class=\"mb-5\" style=\"scroll-margin-top: 24px;\">';\n\n // Collection header\n html += ' <div class=\"d-flex align-items-center gap-3 mb-3\">';\n html += ' <div class=\"rounded-2 d-flex align-items-center justify-content-center flex-shrink-0\" style=\"width: 36px; height: 36px; background: ' + accentColor + '18;\">';\n html += ' <i class=\"fas fa-table\" style=\"color: ' + accentColor + '; font-size: 14px;\"></i>';\n html += ' </div>';\n html += ' <div>';\n html += ' <h2 class=\"fw-bold mb-0\" style=\"font-size: 22px; color: #0f172a;\">' + col.name + '</h2>';\n html += ' <div class=\"text-muted\" style=\"font-size: 12px;\">' + col.fields.length + ' fields' + (col.relations && col.relations.length > 0 ? ' \u00B7 ' + col.relations.length + ' relation' + (col.relations.length > 1 ? 's' : '') : '') + '</div>';\n html += ' </div>';\n html += ' <button class=\"btn btn-sm ms-auto fw-bold px-3\" style=\"border: 1.5px solid ' + accentColor + '; color: ' + accentColor + '; border-radius: 8px; font-size: 12px;\" onclick=\"(window).setEditorTemplate(\\'' + col.name + '\\'); (window).switchView(\\'playground\\')\">';\n html += ' <i class=\"fas fa-play me-1\" style=\"font-size: 10px;\"></i>Try in Playground';\n html += ' </button>';\n html += ' </div>';\n\n html += ' <p class=\"text-muted mb-4\" style=\"font-size: 14px; line-height: 1.7;\">The <strong class=\"text-dark\">' + col.name + '</strong> collection stores records in your QuickQL data layer. Query it directly by name, select specific fields using comma-separated lists, and traverse relations by nesting collection names inline.</p>';\n\n // Fields table\n html += ' <div class=\"mb-4 rounded-2 overflow-hidden\" style=\"border: 1px solid #e2e8f0;\">';\n html += ' <div class=\"px-4 py-3 d-flex align-items-center gap-2\" style=\"background: #f8fafc; border-bottom: 1px solid #e2e8f0;\">';\n html += ' <i class=\"fas fa-list-ul text-muted\" style=\"font-size: 12px;\"></i>';\n html += ' <span class=\"fw-bold text-muted text-uppercase\" style=\"font-size: 11px; letter-spacing: 1px;\">Fields</span>';\n html += ' </div>';\n html += ' <table class=\"w-100 m-0\" style=\"border-collapse: collapse; font-size: 13px;\">';\n html += ' <thead>';\n html += ' <tr style=\"background: #fafafa; border-bottom: 1px solid #f1f5f9;\">';\n html += ' <th class=\"px-4 py-3 text-muted fw-bold text-uppercase\" style=\"font-size: 10px; letter-spacing: 1px; width: 30%;\">Field</th>';\n html += ' <th class=\"px-4 py-3 text-muted fw-bold text-uppercase\" style=\"font-size: 10px; letter-spacing: 1px;\">Description</th>';\n html += ' <th class=\"px-4 py-3 text-muted fw-bold text-uppercase\" style=\"font-size: 10px; letter-spacing: 1px; width: 100px; text-align: right;\">Type</th>';\n html += ' </tr>';\n html += ' </thead>';\n html += ' <tbody>';\n col.fields.forEach((f, fi) => {\n const fName = f.toLowerCase();\n const typeInfo = getTypeInfo(fName);\n const desc = getFieldDesc(fName, col.name);\n html += '<tr style=\"border-bottom: 1px solid #f8fafc; ' + (fi % 2 === 0 ? 'background: #fff;' : 'background: #fafafa;') + '\">';\n html += ' <td class=\"px-4 py-3\"><code style=\"font-size: 13px; font-weight: 700; color: ' + accentColor + '; background: ' + accentColor + '10; padding: 2px 8px; border-radius: 4px; font-family: JetBrains Mono, monospace;\">' + f + '</code></td>';\n html += ' <td class=\"px-4 py-3 text-muted\" style=\"line-height: 1.6;\">' + desc + '</td>';\n html += ' <td class=\"px-4 py-3\" style=\"text-align: right;\"><span class=\"fw-bold\" style=\"font-size: 11px; padding: 3px 10px; border-radius: 100px; background: ' + typeInfo.bg + '; color: ' + typeInfo.color + ';\">' + typeInfo.label + '</span></td>';\n html += '</tr>';\n });\n html += ' </tbody>';\n html += ' </table>';\n html += ' </div>';\n\n // Relations block\n if (col.relations && col.relations.length > 0) {\n html += ' <div class=\"mb-4 rounded-2 overflow-hidden\" style=\"border: 1px solid #e2e8f0;\">';\n html += ' <div class=\"px-4 py-3 d-flex align-items-center gap-2\" style=\"background: #f8fafc; border-bottom: 1px solid #e2e8f0;\">';\n html += ' <i class=\"fas fa-code-branch text-muted\" style=\"font-size: 12px;\"></i>';\n html += ' <span class=\"fw-bold text-muted text-uppercase\" style=\"font-size: 11px; letter-spacing: 1px;\">Relations</span>';\n html += ' </div>';\n col.relations.forEach(rel => {\n html += '<div class=\"px-4 py-3 d-flex align-items-center justify-content-between\" style=\"border-bottom: 1px solid #f8fafc;\">';\n html += ' <div class=\"d-flex align-items-center gap-3\">';\n html += ' <div class=\"d-flex align-items-center justify-content-center rounded-2\" style=\"width: 28px; height: 28px; background: #eff6ff;\">';\n html += ' <i class=\"fas fa-arrow-right text-primary\" style=\"font-size: 10px;\"></i>';\n html += ' </div>';\n html += ' <div>';\n html += ' <div class=\"fw-bold text-dark\" style=\"font-size: 13px;\">' + rel.name + '</div>';\n html += ' <div class=\"text-muted\" style=\"font-size: 11px;\">Points to \u2192 <strong>' + rel.target + '</strong> collection</div>';\n html += ' </div>';\n html += ' </div>';\n html += ' <button class=\"btn btn-sm text-primary fw-bold p-0 text-decoration-none\" style=\"font-size: 12px;\" onclick=\"document.getElementById(\\'doc-' + rel.target + '\\')?.scrollIntoView({behavior:\\'smooth\\'})\">View ' + rel.target + ' \u2192</button>';\n html += '</div>';\n });\n html += ' </div>';\n }\n\n // Sample query\n html += ' <div class=\"mb-2\">';\n html += ' <div class=\"fw-bold text-muted text-uppercase mb-2\" style=\"font-size: 11px; letter-spacing: 1px;\"><i class=\"fas fa-code me-1\"></i> Sample Query</div>';\n html += ' <div class=\"rounded-2 overflow-hidden\" style=\"border: 1px solid #1e293b;\">';\n html += ' <div class=\"px-4 py-2 d-flex align-items-center justify-content-between\" style=\"background: #1e293b;\">';\n html += ' <span class=\"text-muted\" style=\"font-size: 11px; font-family: monospace;\">quickql \u00B7 ' + col.name.toLowerCase() + '</span>';\n html += ' <button class=\"btn btn-sm p-0 fw-bold\" style=\"font-size: 11px; color: #94a3b8;\" onclick=\"(window).setEditorTemplate(\\'' + col.name + '\\'); (window).switchView(\\'playground\\')\">Copy to Editor \u2197</button>';\n html += ' </div>';\n html += ' <pre class=\"m-0 p-4 font-monospace\" style=\"background: #0f172a; color: #e2e8f0; font-size: 13px; line-height: 1.7;\">';\n const sampleFields = col.fields.slice(0, Math.min(col.fields.length, 4)).join(',\\n ');\n html += col.name.toLowerCase() + ' {\\n ' + sampleFields + '\\n}';\n if (col.relations && col.relations.length > 0) {\n html += '\\n\\n' + col.name.toLowerCase() + ' {\\n id,\\n ' + col.relations[0].name + ' {\\n id\\n }\\n}';\n }\n html += '</pre>';\n html += ' </div>';\n html += ' </div>';\n\n html += ' <div style=\"margin: 40px 0; border-top: 1px solid #f1f5f9;\"></div>';\n html += '</section>';\n });\n\n // Footer\n html += ' <div class=\"text-center py-5\" style=\"opacity: 0.2;\">';\n html += ' <img src=\"https://cdn-static.udinmo.com/content/logo/quickql.png\" style=\"height: 20px; filter: grayscale(1);\">';\n html += ' <div class=\"mt-2 fw-bold text-uppercase\" style=\"font-size: 10px; letter-spacing: 3px;\">End of Reference</div>';\n html += ' </div>';\n\n html += ' </div>';\n html += '</main>';\n html += '</div>';\n\n // Hover styles for sidebar nav items\n setTimeout(() => {\n document.querySelectorAll('.docs-nav-item').forEach(el => {\n el.addEventListener('mouseenter', () => { el.style.background = '#f1f5f9'; });\n el.addEventListener('mouseleave', () => { el.style.background = ''; });\n });\n }, 50);\n\n return html;\n}\n\n(function() { \n const config = (window).QuickQLConfig || { endpoint: '/', debug: false };\n \n // UI Architecture\n const app = document.getElementById('app');\n if (app) app.innerHTML = `\n <div class=\"d-flex\" style=\"height: 100vh; background: #f8fafc;\">\n <!-- SIDEBAR -->\n <aside class=\"sidebar-supreme bg-white border-end shadow-sm d-flex flex-column flex-shrink-0\" style=\"width: 280px; z-index: 100;\">\n <div class=\"p-4 border-bottom bg-white d-flex align-items-center justify-content-between\" style=\"height: 72px;\">\n <img src=\"https://cdn-static.udinmo.com/content/logo/quickql.png\" alt=\"QuickQL\" style=\"height: 28px; width: auto;\">\n <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\">\n <i class=\"fas fa-info-circle\"></i>\n </button>\n </div>\n <div class=\"sidebar-content flex-grow-1 p-0 d-flex flex-column\" style=\"overflow: hidden;\">\n <div class=\"px-4 pt-3 pb-0 d-flex align-items-center justify-content-between\">\n <label class=\"text-uppercase small fw-bold text-muted mb-0\" style=\"letter-spacing: 2px; font-size: 10px;\">Explorer</label>\n <div class=\"d-flex gap-2\">\n <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>\n <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>\n </div>\n </div>\n\n <div class=\"p-3\">\n <div class=\"input-group input-group-sm bg-light rounded-3 px-2 border\" style=\"border-radius: 8px !important;\">\n <span class=\"input-group-text bg-transparent border-0 text-muted\"><i class=\"fas fa-search x-small\"></i></span>\n <input type=\"text\" id=\"schema-search\" class=\"form-control bg-transparent border-0 x-small\" placeholder=\"Filter collections...\" style=\"box-shadow: none;\">\n </div>\n </div>\n\n <div id=\"schema-list\" class=\"flex-grow-1 overflow-auto d-flex flex-column\">\n <div class=\"text-center p-5 opacity-25\"><i class=\"fas fa-spinner fa-spin\"></i></div>\n </div>\n </div>\n\n <div class=\"p-3 border-top bg-light\">\n <div class=\"d-flex align-items-center justify-content-between small\">\n <span class=\"text-muted\"><i class=\"fas fa-signal me-1 text-success\"></i> Connected</span>\n <span class=\"badge bg-white text-dark border fw-bold\">LOCAL</span>\n </div>\n </div>\n </aside>\n\n <!-- MAIN MAIN -->\n <main class=\"flex-grow-1 d-flex flex-column h-100\" style=\"overflow: hidden; min-width: 0;\">\n <nav class=\"navbar navbar-expand-lg bg-white border-bottom px-4 flex-shrink-0\" style=\"height: 72px;\">\n <div class=\"container-fluid p-0 d-flex justify-content-between align-items-center flex-nowrap\">\n <div class=\"d-flex align-items-center gap-4\">\n <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;\">\n <i class=\"fas fa-play\"></i> Run\n </button>\n <div id=\"status-chip\" class=\"badge rounded-pill bg-light text-secondary px-3 py-2 border\" style=\"font-size: 11px; font-weight: 600;\">\n <i class=\"fas fa-circle me-1\" style=\"font-size: 8px;\"></i> READY\n </div>\n </div>\n\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"d-flex bg-light p-1 rounded-pill border mx-3 shadow-sm\" style=\"font-size: 11px;\">\n <button class=\"nav-view-btn active border-0 px-3 py-1 rounded-pill\" data-view=\"playground\">\n <i class=\"fas fa-terminal me-1\"></i> <span class=\"nav-btn-text\">Playground</span>\n </button>\n <button class=\"nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white\" data-view=\"schema\">\n <i class=\"fas fa-project-diagram me-1\"></i> <span class=\"nav-btn-text\">Schema</span>\n </button>\n <button class=\"nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white\" data-view=\"visualizer\">\n <i class=\"fas fa-cube me-1\"></i> <span class=\"nav-btn-text\">Visualizer</span>\n </button>\n <button class=\"nav-view-btn border-0 px-3 py-1 bg-transparent rounded-pill text-muted hover-bg-white\" data-view=\"docs\">\n <i class=\"fas fa-book-open me-1\"></i> <span class=\"nav-btn-text\">Docs</span>\n </button>\n </div>\n <div id=\"perf-timer\" class=\"text-muted small fw-bold font-monospace bg-light px-3 py-1 rounded-pill\">--- ms</div>\n <div class=\"vr mx-2 text-muted opacity-25\"></div>\n <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\">\n <i class=\"fas fa-cog text-muted\"></i>\n </button>\n </div>\n </div>\n </nav>\n\n <!-- WORKSPACE CONTENT AREA -->\n <div id=\"workspace-container\" class=\"flex-grow-1 overflow-hidden bg-white position-relative\" style=\"min-width: 0;\">\n <div id=\"loader\" class=\"position-absolute top-50 start-50 translate-middle d-none text-muted\">\n <i class=\"fas fa-spinner fa-spin fa-2x\"></i>\n </div>\n <div id=\"workspace-content\" class=\"h-100\" style=\"min-width: 0;\">\n <!-- Dynamic content injected here -->\n </div>\n </div>\n </main>\n </div>\n\n <!-- SETTINGS MODAL -->\n <div class=\"modal fade\" id=\"settings-modal\" tabindex=\"-1\" aria-hidden=\"true\">\n <div class=\"modal-dialog modal-dialog-centered modal-lg\">\n <div class=\"modal-content border-0 shadow-lg\" style=\"border-radius: 20px; overflow: hidden; background: #ffffff;\">\n <div class=\"modal-header border-0 p-4\" style=\"background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);\">\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"bg-white rounded-circle d-flex align-items-center justify-content-center shadow-sm\" style=\"width: 48px; height: 48px;\">\n <i class=\"fas fa-sliders-h text-primary fs-5\"></i>\n </div>\n <div>\n <h5 class=\"modal-title fw-bolder mb-0 text-dark\" style=\"letter-spacing: -0.5px;\">Console Settings</h5>\n <p class=\"text-muted small mb-0\">Configuration and global preferences</p>\n </div>\n </div>\n <button type=\"button\" class=\"btn-close shadow-none\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n </div>\n <div class=\"modal-body p-5\">\n <div class=\"mb-4\">\n <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>\n <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>\n </div>\n <div class=\"row g-4\">\n <div class=\"col-md-6\">\n <div class=\"p-4 border-0 rounded-4 bg-light shadow-sm h-100\" style=\"background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);\">\n <div class=\"d-flex align-items-center gap-3 mb-2\">\n <div class=\"bg-white p-2 rounded-3 shadow-sm\"><i class=\"fas fa-link text-primary\"></i></div>\n <div class=\"small text-muted fw-bold text-uppercase\">ENDPOINT</div>\n </div>\n <div class=\"fw-bold fs-5 text-dark font-monospace text-truncate\" id=\"config-endpoint\">--</div>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"p-4 border-0 rounded-4 bg-light shadow-sm h-100\" style=\"background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);\">\n <div class=\"d-flex align-items-center gap-3 mb-2\">\n <div class=\"bg-white p-2 rounded-3 shadow-sm\"><i class=\"fas fa-power-off text-success\"></i></div>\n <div class=\"small text-muted fw-bold text-uppercase\">MODE</div>\n </div>\n <div class=\"fw-bold fs-5 text-dark font-monospace\" id=\"config-mode\">--</div>\n </div>\n </div>\n </div>\n <div class=\"mt-4 p-4 rounded-4 border\" style=\"background: #f8fafc; border-style: dashed !important;\">\n <div class=\"d-flex align-items-center justify-content-between\">\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"bg-white p-2 rounded-3 shadow-sm text-primary\"><i class=\"fas fa-external-link-alt\"></i></div>\n <div>\n <div class=\"fw-bold text-dark\" style=\"font-size: 14px;\">Official QuickQL Documentation</div>\n <div class=\"text-muted small\">View full API specs, ecosystem guides, and examples.</div>\n </div>\n </div>\n <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>\n </div>\n </div>\n </div>\n <div class=\"modal-footer border-0 p-4 bg-light\">\n <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>\n </div>\n </div>\n </div>\n </div>\n\n <!-- INFO MODAL -->\n <div class=\"modal fade\" id=\"info-modal\" tabindex=\"-1\" aria-hidden=\"true\">\n <div class=\"modal-dialog modal-dialog-centered\">\n <div class=\"modal-content border-0 shadow-lg\" style=\"border-radius: 20px; overflow: hidden; background: linear-gradient(145deg, #1e293b, #0f172a);\">\n <div class=\"modal-body p-5 text-center text-white position-relative\">\n <div class=\"position-absolute top-0 start-50 translate-middle-x w-100\" style=\"height: 4px; background: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899);\"></div>\n <img src=\"https://cdn-static.udinmo.com/content/logo/quickql.png\" class=\"mb-4\" style=\"height: 48px; filter: brightness(0) invert(1);\">\n <h4 class=\"fw-bold mb-2\">QuickQL Console</h4>\n <p class=\"text-white-50 small mb-4\">The supreme data exploration tool.</p>\n <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 \u2014 ${config.debug ? 'Debug' : 'Production'} Mode</div>\n \n <div class=\"mb-4 text-start\">\n <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>\n <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>\n </div>\n\n <div class=\"d-flex justify-content-center gap-3\">\n <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>\n </div>\n </div>\n </div>\n </div>\n </div>\n `;\n\n // Hydrate Modals\n const cfgJson = document.getElementById('config-json');\n if (cfgJson) cfgJson.innerText = JSON.stringify(config, null, 2);\n const cfgEnd = document.getElementById('config-endpoint');\n if (cfgEnd) cfgEnd.innerText = config.endpoint || '/';\n const cfgMode = document.getElementById('config-mode');\n if (cfgMode) cfgMode.innerText = config.debug ? 'Debug' : 'Production';\n const cfgInfo = document.getElementById('info-config-json');\n if (cfgInfo) cfgInfo.innerText = JSON.stringify(config, null, 2);\n\n // Global Navigation Listener (Event Delegation)\n document.addEventListener('click', (e) => {\n const target = e.target;\n if (!target) return;\n const btn = target.closest ? target.closest('.nav-view-btn') : null;\n if (btn) {\n const viewId = btn.getAttribute('data-view');\n console.log('Navigating to:', viewId);\n if (viewId && window.switchView) {\n window.switchView(viewId);\n }\n }\n });\n\n let editor, results, runBtn, statusChip, perfTimer, schemaList, schemaSearch, expandAllBtn, collapseAllBtn, viewButtons, detailsBtn, httpPanel, httpStatus, httpTime, httpHeaders, httpMeta, httpReqCtx, resizer, headersToggle, headersPanel, headerList, addHeaderBtn, gutter;\n let currentView = null;\n\n (window).playgroundState = (window).playgroundState || {\n activeTabId: 0,\n tabs: [\n { id: 0, name: 'Main Query', query: localStorage.getItem('qq_console_query') || 'users {\\n id,\\n username,\\n email\\n}', results: null }\n ]\n };\n\n (window).playgroundHistory = JSON.parse(localStorage.getItem('qq_history') || '[]');\n\n function lookUpElements() {\n editor = document.getElementById('editor');\n results = document.getElementById('results');\n runBtn = document.getElementById('run-btn');\n statusChip = document.getElementById('status-chip');\n perfTimer = document.getElementById('perf-timer');\n schemaList = document.getElementById('schema-list');\n schemaSearch = document.getElementById('schema-search');\n expandAllBtn = document.getElementById('expand-all');\n collapseAllBtn = document.getElementById('collapse-all');\n viewButtons = document.querySelectorAll('.nav-view-btn');\n detailsBtn = document.getElementById('details-btn');\n httpPanel = document.getElementById('http-panel');\n httpStatus = document.getElementById('http-status');\n httpTime = document.getElementById('http-time');\n httpHeaders = document.getElementById('http-headers');\n httpMeta = document.getElementById('http-meta');\n httpReqCtx = document.getElementById('http-req-ctx');\n resizer = document.getElementById('resizer');\n headersToggle = document.getElementById('headers-toggle');\n headersPanel = document.getElementById('headers-panel');\n headerList = document.getElementById('header-list');\n addHeaderBtn = document.getElementById('add-header');\n gutter = document.getElementById('gutter');\n }\n\n // Routing & State Management\n (window).switchView = async (viewId, params = {}) => {\n const content = document.getElementById('workspace-content');\n const loader = document.getElementById('loader');\n if (!content) return;\n \n // Save state if leaving playground\n if (currentView === 'playground' && editor) {\n const state = window.playgroundState;\n const currentTab = state.tabs.find(t => t.id === state.activeTabId);\n if (currentTab) currentTab.query = editor.value;\n }\n\n // Update Nav UI\n const btns = document.querySelectorAll('.nav-view-btn');\n btns.forEach(b => {\n const v = b.getAttribute('data-view');\n const active = v === viewId || (viewId === 'collection' && v === 'schema');\n b.classList.toggle('active', active);\n b.classList.toggle('bg-white', active);\n b.classList.toggle('shadow-sm', active);\n b.classList.toggle('bg-transparent', !active);\n b.classList.toggle('text-muted', !active);\n });\n\n if (loader) loader.classList.remove('d-none');\n \n try {\n if (viewId === 'playground') {\n content.innerHTML = renderPlaygroundPage();\n initPlaygroundLogic();\n } else if (viewId === 'schema') {\n content.innerHTML = await renderSchemaPage();\n } else if (viewId === 'collection') {\n content.innerHTML = renderCollectionPage(params.name);\n } else if (viewId === 'visualizer') {\n // Ensure schema is pre-fetched for the sidebar before rendering\n if (!(window).currentSchema || (window).currentSchema.length === 0) {\n await (window).fetchSchema();\n }\n content.innerHTML = renderVisualizerPage();\n (window).initVisualizer();\n } else if (viewId === 'docs') {\n // Ensure schema is loaded before rendering docs\n if (!(window).currentSchema || (window).currentSchema.length === 0) {\n await (window).fetchSchema();\n }\n content.innerHTML = renderDocsPage();\n }\n } catch(e) {\n console.error('Routing error:', e);\n content.innerHTML = '<div class=\"p-5 text-center text-danger\"><h4>Failed to load page</h4><p>' + e.message + '</p></div>';\n }\n\n if (loader) loader.classList.add('d-none');\n \n currentView = viewId;\n // Push state to URL\n if (viewId !== 'collection') {\n if (window.location.hash !== '#' + viewId) {\n window.location.hash = viewId;\n }\n } else {\n if (window.location.hash !== '#schema') {\n window.location.hash = 'schema';\n }\n }\n };\n\n // PAGE RENDERERS\n function renderPlaygroundPage() {\n const state = (window).playgroundState;\n let html = '';\n html += '<div class=\"d-flex flex-column h-100\" style=\"background: #f1f5f9; min-width: 0;\">';\n html += '<div class=\"playground-tabs bg-white border-bottom d-flex align-items-center px-4\" style=\"height: 48px;\">';\n html += '<div id=\"tabs-container\" class=\"d-flex h-100 align-items-center\">';\n \n state.tabs.forEach(tab => {\n const active = state.activeTabId === tab.id;\n html += '<div class=\"playground-tab ' + (active ? 'active' : '') + '\" ';\n html += 'onclick=\"(window).switchTab(' + tab.id + ')\" ';\n 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') + ';\">';\n html += '<i class=\"fas fa-file-code me-2 opacity-50\"></i> ' + tab.name;\n if (tab.id !== 0 && state.tabs.length > 1) {\n html += '<i class=\"fas fa-times ms-3 opacity-25 hover-opacity-100\" onclick=\"event.stopPropagation(); (window).closeTab(' + tab.id + ')\"></i>';\n }\n html += '</div>';\n });\n\n html += '</div>';\n 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>';\n 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>';\n html += '</div>';\n\n html += '<div class=\"d-flex gap-4 p-4 flex-grow-1 overflow-hidden\" style=\"min-height: 0; min-width: 0;\">';\n html += '<div class=\"card border-0 shadow-sm rounded-4 overflow-hidden d-flex flex-column\" style=\"flex: 1 1 0; min-width: 0;\">';\n 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>';\n html += '<div class=\"card-body p-0 flex-grow-1 d-flex overflow-hidden\">';\n html += '<div class=\"d-flex flex-grow-1\" style=\"background: white; min-width: 0;\">';\n 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>';\n 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>';\n html += '</div>';\n html += '</div>';\n html += '<div class=\"card-footer bg-white border-top p-0 overflow-hidden\">';\n 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>';\n 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>';\n html += '</div>';\n html += '</div>';\n \n 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;\">';\n 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>';\n html += '<div class=\"card-body p-0 flex-grow-1 bg-light overflow-hidden d-flex flex-column\" style=\"min-width: 0;\">';\n 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>';\n html += '</div>';\n html += '<div class=\"card-footer bg-white border-top p-0 overflow-hidden\">';\n 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>';\n html += '<div id=\"http-panel\" class=\"p-0 border-top bg-light d-none font-monospace small\" style=\"height: 300px; overflow: hidden;\">';\n html += '<div class=\"d-flex h-100\">';\n html += '<div class=\"d-flex flex-column border-end bg-white\" style=\"width: 220px;\">';\n 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>';\n html += '<div id=\"history-list\" class=\"flex-grow-1 overflow-auto p-2\" style=\"background: #fafafa;\"></div>';\n html += '</div>';\n html += '<div class=\"flex-grow-1 p-4 overflow-auto bg-white\">';\n html += '<div class=\"row g-3 px-2 mb-2\">';\n 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>';\n 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>';\n html += '</div>';\n html += '<hr class=\"opacity-10\">';\n html += '<div class=\"fw-bold x-small text-muted mb-2\">RESPONSE HEADERS (incl. Cookies)</div>';\n 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>';\n html += '<div class=\"fw-bold x-small text-muted mb-2\">REQUEST PAYLOAD</div>';\n 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>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n \n return html;\n }\n\n async function renderSchemaPage() {\n const res = await fetch(config.endpoint + '?schema=true');\n const schema = await res.json();\n (window).currentSchema = schema;\n\n let html = '';\n html += '<div class=\"overflow-auto p-5 h-100 bg-white\">';\n html += '<div class=\"container-tight mx-auto\" style=\"max-width: 1200px;\">';\n html += '<div class=\"d-flex align-items-center justify-content-between mb-4 pb-4 border-bottom\">';\n 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>';\n 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>';\n html += '</div>';\n\n // Search Input\n html += '<div class=\"mb-5\">';\n html += '<div class=\"input-group shadow-sm border border-light rounded-pill overflow-hidden bg-white\">';\n html += '<span class=\"input-group-text bg-transparent border-0 text-muted px-4\"><i class=\"fas fa-search\"></i></span>';\n 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;\">';\n html += '</div>';\n html += '</div>';\n\n html += '<div id=\"schema-summary-full\" class=\"row g-4 mb-5\">';\n \n schema.forEach(col => {\n html += '<div class=\"col-md-6 col-lg-4 schema-card-container\" data-name=\"' + col.name.toLowerCase() + '\">';\n html += '<div class=\"card border-0 shadow-sm rounded-0 p-4 h-100 bg-light\" style=\"border: 1px solid #e2e8f0 !important;\">';\n 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>';\n html += '<div class=\"d-flex flex-wrap gap-2\">';\n col.fields.forEach(f => {\n html += '<span class=\"badge bg-white text-dark border rounded-0 px-2 py-1 font-monospace x-small shadow-sm\">' + f + '</span>';\n });\n html += '</div>';\n html += '</div>';\n html += '</div>';\n });\n\n html += '</div>';\n\n // Branding Footer\n html += ' <div class=\"text-center py-5 opacity-25 mt-auto\">';\n html += ' <img src=\"https://cdn-static.udinmo.com/content/logo/quickql.png\" style=\"height: 24px; filter: grayscale(1);\">';\n html += ' <p class=\"x-small mt-2 fw-bold text-uppercase\" style=\"letter-spacing: 2px;\">QuickQL Framework Explorer</p>';\n html += ' </div>';\n\n html += '</div>';\n html += '</div>';\n\n setTimeout(() => {\n const searchInput = document.getElementById('schema-explorer-search');\n if (searchInput) {\n searchInput.addEventListener('input', (e) => {\n const q = e.target.value.toLowerCase();\n const cards = document.querySelectorAll('.schema-card-container');\n cards.forEach(card => {\n if (card.getAttribute('data-name').includes(q)) {\n card.classList.remove('d-none');\n } else {\n card.classList.add('d-none');\n }\n });\n });\n }\n }, 50);\n\n return html;\n }\n\n function renderCollectionPage(name) {\n const schema = (window).currentSchema || [];\n const col = schema.find(s => s.name === name);\n if (!col) return 'Collection not found';\n \n let html = '';\n html += '<div class=\"overflow-auto p-5 h-100 bg-white\">';\n html += '<div class=\"container mx-auto\" style=\"max-width: 1000px;\">';\n html += '<button class=\"btn btn-link p-0 text-muted mb-4\" onclick=\"(window).switchView(\\'schema\\')\">Back</button>';\n html += '<h1 class=\"fw-bold mb-4\">' + col.name + ' Documentation</h1>';\n html += '<div class=\"card border-0 bg-light p-4 rounded-4\">';\n html += '<h5>Fields</h5>';\n html += '<ul>';\n col.fields.forEach(f => {\n html += '<li>' + f + '</li>';\n });\n html += '</ul>';\n html += '<button class=\"btn btn-dark mt-4\" onclick=\"(window).setEditorTemplate(\\'' + col.name + '\\'); (window).switchView(\\'playground\\')\">Use in Playground</button>';\n html += '</div>';\n html += '</div>';\n html += '</div>';\n return html;\n }\n\n function renderVisualizerPage() {\n return `\n <div class=\"bg-light position-relative overflow-hidden h-100\">\n <div class=\"position-absolute top-0 start-0 p-4 z-3 d-flex gap-3\">\n <button class=\"btn btn-white btn-sm rounded-pill px-4 fw-bold shadow-sm border\" onclick=\"(window).switchView('schema')\">Exit</button>\n </div>\n <canvas id=\"visualizer-canvas\" class=\"w-100 h-100\"></canvas>\n </div>\n `;\n }\n\n // Documentation logic imported from docs.ts\n\n\n (window).showCollectionDocs = (name) => {\n (window).switchView('collection', { name });\n };\n\n function initPlaygroundLogic() {\n lookUpElements();\n renderHistory();\n \n if (addHeaderBtn) {\n addHeaderBtn.onclick = () => {\n const row = document.createElement('div');\n row.className = 'header-row d-flex gap-2';\n row.innerHTML = `\n <input type=\"text\" class=\"form-control form-control-sm font-monospace x-small h-key\" placeholder=\"Key\" style=\"border-radius: 6px;\">\n <input type=\"text\" class=\"form-control form-control-sm font-monospace x-small h-value\" placeholder=\"Value\" style=\"border-radius: 6px;\">\n <button class=\"btn btn-sm btn-link text-danger p-1 border-0 del-header\"><i class=\"fas fa-times-circle\"></i></button>\n `;\n row.querySelector('.del-header')?.addEventListener('click', () => row.remove());\n headerList?.appendChild(row);\n };\n }\n\n if (headerList && headerList.children.length === 0) addHeaderBtn?.click();\n\n if (headersToggle) {\n headersToggle.onclick = () => {\n headersPanel?.classList.toggle('d-none');\n headersToggle.querySelector('.fas.fa-chevron-up')?.classList.toggle('fa-rotate-180');\n };\n }\n\n const httpToggle = document.getElementById('http-toggle');\n if (httpToggle) {\n httpToggle.onclick = () => {\n httpPanel?.classList.toggle('d-none');\n httpToggle.querySelector('.fas.fa-chevron-up')?.classList.toggle('fa-rotate-180');\n };\n }\n\n if (editor) {\n const state = window.playgroundState;\n const currentTab = state.tabs.find(t => t.id === state.activeTabId);\n if (currentTab) {\n editor.value = currentTab.query;\n if (results && currentTab.results) {\n results.innerHTML = currentTab.results;\n } else if (results) {\n results.innerHTML = '<div class=\"p-5 text-center text-muted opacity-25\">Ready to execute query</div>';\n }\n }\n editor.oninput = updateLineNumbers;\n editor.onscroll = () => {\n if (gutter) gutter.scrollTop = editor.scrollTop;\n };\n updateLineNumbers();\n editor.focus();\n }\n\n if (runBtn) runBtn.onclick = execute;\n }\n\n window.switchTab = (tabId) => {\n const state = window.playgroundState;\n const currentTab = state.tabs.find(t => t.id === state.activeTabId);\n if (currentTab) {\n if (editor) currentTab.query = editor.value;\n if (results) currentTab.results = results.innerHTML;\n }\n state.activeTabId = tabId;\n window.switchView('playground');\n };\n\n window.addTab = () => {\n const state = window.playgroundState;\n const newId = Math.max(...state.tabs.map(t => t.id), -1) + 1;\n state.tabs.push({\n id: newId,\n name: `New Query ${newId + 1}`,\n query: '',\n results: null\n });\n window.switchTab(newId);\n };\n\n window.closeTab = (tabId) => {\n const state = window.playgroundState;\n if (state.tabs.length <= 1) return;\n state.tabs = state.tabs.filter(t => t.id !== tabId);\n if (state.activeTabId === tabId) state.activeTabId = state.tabs[0].id;\n window.switchView('playground');\n };\n\n function updateLineNumbers() {\n if (!editor || !gutter) return;\n const text = editor.value;\n const lines = text.split('\\n').length;\n let gutterContent = '';\n for (let i = 1; i <= lines; i++) {\n gutterContent += '<div style=\"line-height: 1.6; font-size: 13px; padding: 0 8px; color: #94a3b8;\">' + i + '</div>';\n }\n gutter.innerHTML = gutterContent;\n }\n\n\n (window).setEditorTemplate = (model) => {\n const query = `${model} {\\n id,\\n *\\n}`;\n const state = (window).playgroundState;\n const currentTab = state.tabs.find(t => t.id === state.activeTabId);\n if (currentTab) currentTab.query = query;\n if (editor) {\n editor.value = query;\n updateLineNumbers();\n editor.focus();\n }\n localStorage.setItem('qq_console_query', query);\n };\n\n (window).fetchSchema = async () => {\n try {\n const res = await fetch(config.endpoint + '?schema=true');\n const data = await res.json();\n (window).currentSchema = data;\n \n const schemaListEl = document.getElementById('schema-list');\n if (!schemaListEl) return;\n let html = '';\n data.forEach(col => {\n html += '<div class=\"border-bottom\" data-name=\"' + col.name + '\">';\n html += '<div class=\"px-4 py-2 hover-bg-light d-flex align-items-center gap-3 collection-item\" ';\n html += 'style=\"cursor: pointer; min-height: 48px;\" ';\n html += 'data-bs-target=\"#col-' + col.name + '\">';\n html += '<i class=\"fas fa-chevron-right text-muted transition-rotate\" style=\"font-size: 10px;\"></i>';\n html += '<div class=\"d-flex flex-column\">';\n html += '<span class=\"fw-bold text-dark small\">' + col.name + '</span>';\n html += '<span class=\"text-muted x-small opacity-75 fw-normal\">' + col.fields.length + ' fields</span>';\n html += '</div>';\n html += '<button class=\"btn btn-sm btn-link ms-auto p-1 text-primary opacity-50 hover-opacity-100 stop-prop\">';\n html += '<i class=\"fas fa-plus\"></i>';\n html += '</button>';\n html += '</div>';\n html += '<div class=\"collapse\" id=\"col-' + col.name + '\">';\n html += '<div class=\"px-4 pb-3 pt-2 d-flex flex-column gap-1\" style=\"margin-left: 36px; border-left: 2px solid #f1f5f9;\">';\n col.fields.forEach(f => {\n html += '<div class=\"x-small text-dark fw-bold py-1 d-flex align-items-center justify-content-between group-hover-visible\">';\n html += '<div class=\"d-flex align-items-center gap-2\"><i class=\"fas fa-hashtag opacity-25\" style=\"font-size: 10px;\"></i>' + f + '</div>';\n 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>';\n html += '</div>';\n });\n html += '</div>';\n html += '</div>';\n html += '</div>';\n });\n schemaListEl.innerHTML = html;\n\n const sSearch = document.getElementById('schema-search');\n if (sSearch) {\n sSearch.oninput = (e) => {\n const q = (e.target).value.toLowerCase();\n const items = schemaListEl.querySelectorAll('.border-bottom[data-name]');\n items.forEach(item => {\n if (item.getAttribute('data-name').includes(q)) {\n item.classList.remove('d-none');\n } else {\n item.classList.add('d-none');\n }\n });\n };\n }\n\n document.querySelectorAll('.collection-item').forEach(item => {\n (item).onclick = (e) => {\n const name = item.getAttribute('data-name');\n if (name) (window).showCollectionDocs(name);\n const targetId = item.getAttribute('data-bs-target');\n const targetEl = targetId ? document.querySelector(targetId) : null;\n if (targetEl) {\n const isShowing = targetEl.classList.contains('show');\n const bsCollapse = new (window).bootstrap.Collapse(targetEl, { toggle: false });\n if (isShowing) bsCollapse.hide(); else bsCollapse.show();\n }\n };\n });\n\n const expandAll = document.getElementById('expand-all');\n const collapseAll = document.getElementById('collapse-all');\n if (expandAll) {\n expandAll.onclick = () => {\n document.querySelectorAll('.collapse').forEach(el => {\n try { new (window).bootstrap.Collapse(el, { toggle: false }).show(); } catch(e){}\n });\n };\n }\n if (collapseAll) {\n collapseAll.onclick = () => {\n document.querySelectorAll('.collapse').forEach(el => {\n try { new (window).bootstrap.Collapse(el, { toggle: false }).hide(); } catch(e){}\n });\n };\n }\n } catch (e) {\n console.error('Schema fetch error:', e);\n }\n };\n\n async function execute() {\n lookUpElements();\n if (!editor || !results) return;\n const query = editor.value.trim();\n if (!query) return;\n localStorage.setItem('qq_console_query', query);\n if (runBtn) runBtn.disabled = true;\n results.style.opacity = '0.5';\n\n // Grab custom headers from UI\n const customHeaders = {};\n if (headerList) {\n headerList.querySelectorAll('.header-row').forEach(row => {\n const kInput = row.querySelector('.h-key');\n const vInput = row.querySelector('.h-value');\n if (kInput && vInput) {\n const k = kInput.value.trim();\n const v = vInput.value.trim();\n if (k && v) customHeaders[k] = v;\n }\n });\n }\n \n const startTime = performance.now();\n const payload = { query };\n\n try {\n if (httpMeta) httpMeta.innerText = JSON.stringify(payload, null, 2);\n \n const res = await fetch(config.endpoint, {\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, customHeaders),\n body: JSON.stringify(payload)\n });\n const data = await res.json();\n const duration = Math.round(performance.now() - startTime);\n \n if (httpStatus) {\n const isErr = res.status >= 400;\n httpStatus.innerHTML = '<span class=\"' + (isErr ? 'text-danger' : 'text-success') + ' fw-bold\">' + res.status + ' ' + res.statusText + '</span>';\n }\n if (httpTime) httpTime.innerText = duration + 'ms';\n if (httpHeaders) {\n let hStr = '';\n try {\n for (let [k, v] of res.headers.entries()) hStr += '<b>' + k + '</b>: <span class=\"text-muted\">' + v + '</span><br>';\n } catch(e) {}\n httpHeaders.innerHTML = hStr || '--';\n }\n\n results.innerHTML = typeof data === 'string' ? data : JSON.stringify(data, null, 2);\n const state = window.playgroundState;\n const currentTab = state.tabs.find(t => t.id === state.activeTabId);\n if (currentTab) currentTab.results = results.innerHTML;\n \n saveHistory({ payload, data, status: res.status, statusText: res.statusText, duration, headers: Object.fromEntries(res.headers.entries()) });\n if (perfTimer) perfTimer.innerText = duration + 'ms';\n } catch (e) {\n results.innerHTML = 'Error: ' + e.message;\n saveHistory({ payload: { query: editor.value }, data: { error: e.message }, status: 0, statusText: 'Fetch Error', duration: 0, headers: {} });\n if (httpStatus) httpStatus.innerHTML = '<span class=\"text-danger fw-bold\">ERROR</span>';\n if (httpTime) httpTime.innerText = '--';\n if (httpHeaders) httpHeaders.innerHTML = '--';\n } finally {\n if (runBtn) runBtn.disabled = false;\n results.style.opacity = '1';\n }\n }\n\n function saveHistory(item) {\n const hist = (window).playgroundHistory;\n hist.unshift({ ...item, id: Date.now(), timestamp: new Date().toLocaleTimeString() });\n if (hist.length > 25) hist.pop();\n localStorage.setItem('qq_history', JSON.stringify(hist));\n renderHistory();\n }\n\n function renderHistory() {\n const hList = document.getElementById('history-list');\n if (!hList) return;\n const hist = (window).playgroundHistory;\n hList.innerHTML = hist.map((h, i) => `\n <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})\">\n <div class=\"d-flex align-items-center justify-content-between mb-1\">\n <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>\n <span class=\"x-small text-muted\">${h.timestamp}</span>\n </div>\n <div class=\"x-small text-truncate text-muted opacity-75 font-monospace\">${JSON.stringify(h.payload.query).substring(0, 40)}...</div>\n <div class=\"text-end x-small text-muted mt-1 fw-bold opacity-50\">${h.duration}ms</div>\n </div>\n `).join('') || '<div class=\"text-center p-3 opacity-25 x-small\">No history</div>';\n }\n\n (window).restoreHistory = (id) => {\n const hist = (window).playgroundHistory;\n const item = hist.find(h => h.id === id);\n if (!item) return;\n\n if (results) results.innerHTML = JSON.stringify(item.data, null, 2);\n if (httpStatus) httpStatus.innerHTML = '<span class=\"' + (item.status >= 400 || item.status === 0 ? 'text-danger' : 'text-success') + ' fw-bold\">' + item.status + ' ' + item.statusText + '</span>';\n if (httpTime) httpTime.innerText = item.duration + 'ms';\n if (httpMeta) httpMeta.innerText = JSON.stringify(item.payload, null, 2);\n if (httpHeaders) {\n let hStr = '';\n for (let k in item.headers) hStr += '<b>' + k + '</b>: <span class=\"text-muted\">' + item.headers[k] + '</span><br>';\n httpHeaders.innerHTML = hStr || '--';\n }\n\n // Highlight selected in sidebar\n document.querySelectorAll('.history-item').forEach(el => el.classList.remove('border-primary'));\n const active = Array.from(document.querySelectorAll('.history-item')).find(el => el.getAttribute('onclick')?.includes(id));\n if (active) active.classList.add('border-primary');\n };\n\n // Visualizer Logic\n (window).visNodes = [];\n (window).visOffset = { x: 0, y: 0 };\n (window).dragNode = null;\n (window).dragOffset = { x: 0, y: 0 };\n\n (window).initVisualizer = async () => {\n if(!(window).visInitialized) {\n const res = await fetch(config.endpoint + '?schema=true');\n const data = await res.json();\n (window).visNodes = data.map((s, i) => ({\n name: s.name, x: 200 + (i % 2) * 450, y: 200 + Math.floor(i / 2) * 400,\n w: 280, h: 80 + s.fields.length * 26 + (s.relations?.length || 0) * 26, fields: s.fields, relations: s.relations\n }));\n\n // Restore drag and pan positions from cache\n const savedPositions = localStorage.getItem('quickql_vis_positions');\n if (savedPositions) {\n try {\n const pos = JSON.parse(savedPositions);\n (window).visNodes.forEach(node => {\n if (pos[node.name]) {\n node.x = pos[node.name].x;\n node.y = pos[node.name].y;\n }\n });\n } catch(e) {}\n }\n const savedOffset = localStorage.getItem('quickql_vis_offset');\n if (savedOffset) {\n try {\n (window).visOffset = JSON.parse(savedOffset);\n } catch(e) {}\n }\n\n (window).visInitialized = true;\n }\n\n const canvas = document.getElementById('visualizer-canvas');\n if (canvas) {\n canvas.onmousedown = (e) => {\n const rect = canvas.getBoundingClientRect();\n const mouseX = e.clientX - rect.left - (window).visOffset.x;\n const mouseY = e.clientY - rect.top - (window).visOffset.y;\n \n let foundNode = false;\n for (let i = ((window).visNodes || []).length - 1; i >= 0; i--) {\n const n = (window).visNodes[i];\n if (mouseX >= n.x && mouseX <= n.x + n.w && mouseY >= n.y && mouseY <= n.y + n.h) {\n (window).dragNode = n;\n (window).dragOffset = { x: mouseX - n.x, y: mouseY - n.y };\n canvas.style.cursor = 'grabbing';\n foundNode = true;\n break;\n }\n }\n\n if (!foundNode) {\n (window).isPanning = true;\n (window).panStart = { x: e.clientX, y: e.clientY };\n canvas.style.cursor = 'all-scroll';\n }\n };\n\n canvas.onmousemove = (e) => {\n if ((window).dragNode) {\n const rect = canvas.getBoundingClientRect();\n const mouseX = e.clientX - rect.left - (window).visOffset.x;\n const mouseY = e.clientY - rect.top - (window).visOffset.y;\n \n let newX = mouseX - (window).dragOffset.x;\n let newY = mouseY - (window).dragOffset.y;\n\n (window).dragNode.x = newX;\n (window).dragNode.y = newY;\n (window).drawVis();\n } else if ((window).isPanning) {\n const dx = e.clientX - (window).panStart.x;\n const dy = e.clientY - (window).panStart.y;\n (window).visOffset.x += dx;\n (window).visOffset.y += dy;\n (window).panStart = { x: e.clientX, y: e.clientY };\n (window).drawVis();\n } else {\n const rect = canvas.getBoundingClientRect();\n const mouseX = e.clientX - rect.left - (window).visOffset.x;\n const mouseY = e.clientY - rect.top - (window).visOffset.y;\n let hovering = false;\n for (let i = 0; i < ((window).visNodes || []).length; i++) {\n const n = (window).visNodes[i];\n if (mouseX >= n.x && mouseX <= n.x + n.w && mouseY >= n.y && mouseY <= n.y + n.h) {\n hovering = true;\n break;\n }\n }\n canvas.style.cursor = hovering ? 'grab' : 'default';\n }\n };\n\n canvas.onmouseup = () => {\n (window).dragNode = null;\n (window).isPanning = false;\n if (canvas) canvas.style.cursor = 'default';\n };\n\n canvas.onmouseleave = () => {\n (window).dragNode = null;\n (window).isPanning = false;\n if (canvas) canvas.style.cursor = 'default';\n };\n }\n\n (window).drawVis();\n };\n\n (window).drawVis = () => {\n const visualizerCanvas = document.getElementById('visualizer-canvas');\n const visualizerCtx = visualizerCanvas ? visualizerCanvas.getContext('2d') : null;\n if (!visualizerCanvas || !visualizerCtx) return;\n visualizerCanvas.width = visualizerCanvas.parentElement.clientWidth;\n visualizerCanvas.height = visualizerCanvas.parentElement.clientHeight;\n visualizerCtx.clearRect(0, 0, visualizerCanvas.width, visualizerCanvas.height);\n \n // Save bounds real-time internally to storage\n if ((window).visNodes && (window).visNodes.length > 0 && (window).visInitialized) {\n const positions = {};\n (window).visNodes.forEach(node => {\n positions[node.name] = { x: node.x, y: node.y };\n });\n localStorage.setItem('quickql_vis_positions', JSON.stringify(positions));\n localStorage.setItem('quickql_vis_offset', JSON.stringify((window).visOffset));\n }\n \n // Draw Dots Grid\n visualizerCtx.fillStyle = '#cbd5e1';\n const dotSpacing = 20;\n const offsetX = ((window).visOffset.x % dotSpacing + dotSpacing) % dotSpacing;\n const offsetY = ((window).visOffset.y % dotSpacing + dotSpacing) % dotSpacing;\n for (let x = offsetX; x < visualizerCanvas.width; x += dotSpacing) {\n for (let y = offsetY; y < visualizerCanvas.height; y += dotSpacing) {\n visualizerCtx.fillRect(x, y, 2, 2);\n }\n }\n \n visualizerCtx.save();\n visualizerCtx.translate((window).visOffset.x, (window).visOffset.y);\n\n // Draw Relation Wires first (behind nodes)\n ((window).visNodes || []).forEach(node => {\n if (node.relations) {\n node.relations.forEach((rel, ri) => {\n const targetNode = (window).visNodes.find(n => n.name === rel.target);\n if (targetNode) {\n visualizerCtx.beginPath();\n \n let startX, endX, cp1x, cp2x;\n const startY = node.y + 80 + ((node.fields?.length || 0) + ri) * 26 + 10; // mid of relation label\n const endY = targetNode.y + 32; // mid of target header\n \n let arrowPointLeft = false;\n\n if (node.x + node.w / 2 < targetNode.x + targetNode.w / 2) {\n // Origin node is to the left of Target node\n startX = node.x + node.w; // Start from right edge\n endX = targetNode.x - 4; // End at left edge\n cp1x = startX + 100;\n cp2x = endX - 100;\n arrowPointLeft = false;\n } else {\n // Origin node is to the right of Target node\n startX = node.x; // Start from left edge\n endX = targetNode.x + targetNode.w + 4; // End at right edge\n cp1x = startX - 100;\n cp2x = endX + 100;\n arrowPointLeft = true;\n }\n\n visualizerCtx.moveTo(startX, startY);\n // Bezier curve for wires\n visualizerCtx.bezierCurveTo(cp1x, startY, cp2x, endY, endX, endY);\n visualizerCtx.strokeStyle = '#94a3b8';\n visualizerCtx.lineWidth = 2;\n visualizerCtx.stroke();\n \n // Draw end arrowhead\n visualizerCtx.beginPath();\n visualizerCtx.moveTo(endX, endY);\n if (arrowPointLeft) {\n visualizerCtx.lineTo(endX + 10, endY - 6);\n visualizerCtx.lineTo(endX + 10, endY + 6);\n } else {\n visualizerCtx.lineTo(endX - 10, endY - 6);\n visualizerCtx.lineTo(endX - 10, endY + 6);\n }\n visualizerCtx.closePath();\n visualizerCtx.fillStyle = '#94a3b8';\n visualizerCtx.fill();\n\n // Draw start arrowhead\n visualizerCtx.beginPath();\n visualizerCtx.moveTo(startX, startY);\n if (arrowPointLeft) {\n visualizerCtx.lineTo(startX - 10, startY - 6);\n visualizerCtx.lineTo(startX - 10, startY + 6);\n } else {\n visualizerCtx.lineTo(startX + 10, startY - 6);\n visualizerCtx.lineTo(startX + 10, startY + 6);\n }\n visualizerCtx.closePath();\n visualizerCtx.fillStyle = '#94a3b8';\n visualizerCtx.fill();\n }\n });\n }\n });\n\n ((window).visNodes || []).forEach(node => {\n // Node Box\n visualizerCtx.fillStyle = '#ffffff'; visualizerCtx.shadowBlur = 15; visualizerCtx.shadowColor = 'rgba(0,0,0,0.06)';\n visualizerCtx.beginPath();\n if (visualizerCtx.roundRect) {\n visualizerCtx.roundRect(node.x, node.y, node.w, node.h, 12);\n } else {\n visualizerCtx.rect(node.x, node.y, node.w, node.h);\n }\n visualizerCtx.fill();\n visualizerCtx.strokeStyle = '#e2e8f0'; visualizerCtx.lineWidth = 1; visualizerCtx.stroke();\n visualizerCtx.shadowColor = 'transparent';\n \n // Node Title Header\n visualizerCtx.fillStyle = '#0f172a'; visualizerCtx.font = 'bold 14px sans-serif';\n visualizerCtx.fillText(node.name.toLowerCase(), node.x + 20, node.y + 32);\n\n // Node Divider Line\n visualizerCtx.beginPath();\n visualizerCtx.moveTo(node.x, node.y + 50);\n visualizerCtx.lineTo(node.x + node.w, node.y + 50);\n visualizerCtx.strokeStyle = '#f1f5f9';\n visualizerCtx.stroke();\n\n // Node Fields\n visualizerCtx.fillStyle = '#475569'; visualizerCtx.font = '13px monospace';\n (node.fields || []).forEach((f, fi) => {\n visualizerCtx.fillText(f, node.x + 20, node.y + 80 + (fi * 26));\n });\n \n // Relation Labels inside Node Box (if any)\n if (node.relations) {\n node.relations.forEach((rel, ri) => {\n visualizerCtx.fillStyle = '#3b82f6'; visualizerCtx.font = 'bold 12px monospace';\n visualizerCtx.fillText(rel.name + ' \u2192 ' + rel.target, node.x + 20, node.y + 80 + ((node.fields?.length || 0) + ri) * 26);\n });\n }\n });\n visualizerCtx.restore();\n };\n\n window.addEventListener('hashchange', () => {\n const hash = window.location.hash ? window.location.hash.slice(1) : 'playground';\n if (validViews.includes(hash) && currentView !== hash) {\n (window).switchView(hash);\n }\n });\n\n (window).fetchSchema();\n const initialHash = window.location.hash ? window.location.hash.slice(1) : 'playground';\n const validViews = ['playground', 'schema', 'visualizer', 'docs'];\n (window).switchView(validViews.includes(initialHash) ? initialHash : 'playground');\n})();\n";
|
|
15
|
+
export declare const playgroundCss = "\n@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono&display=swap');\nbody { font-family: 'Plus Jakarta Sans', sans-serif; background: #f8fafc; margin: 0; }\n.font-monospace { font-family: 'JetBrains Mono', monospace !important; }\n.nav-view-btn.active { color: #0d6efd !important; background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }\n.hover-bg-light:hover { background: #f8fafc; }\n.transition-rotate { transition: transform 0.2s; }\n#editor { outline: none !important; box-shadow: none !important; caret-color: #0d6efd; line-height: 1.6; }\n#editor:focus { outline: none; box-shadow: none; }\n#gutter { overflow: hidden; line-height: 1.6; }\n.x-small { font-size: 11px !important; }\npre { tab-size: 2; }\n@media (max-width: 1200px) {\n .nav-btn-text { display: none; }\n #perf-timer { display: none; }\n .vr { display: none; }\n #status-chip { display: none; }\n}\n";
|
|
16
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../../../src/playground/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;GAEG;AACH,eAAO,MAAM,YAAY,+o7EAsjCxB,CAAC;AAEF,eAAO,MAAM,aAAa,m8BAkBzB,CAAC"}
|