gcyphrq 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ export type CypherLiteral = string | number | boolean | null;
2
+ export interface CypherNode {
3
+ id: string;
4
+ label?: string;
5
+ [key: string]: CypherLiteral | undefined;
6
+ }
7
+ export interface CypherEdge {
8
+ id: string;
9
+ type?: string;
10
+ [key: string]: CypherLiteral | undefined;
11
+ }
12
+ /**
13
+ * Pre-computed indexes for fast node/edge lookup during query execution.
14
+ * Built once at graph construction time, used by the engine to avoid
15
+ * full-graph scans during MATCH, WHERE, and traversal.
16
+ *
17
+ * Note: property values are coerced to strings via `String(value)`, so
18
+ * `true`/`false` become `"true"`/`"false"` and `0` becomes `"0"`.
19
+ */
20
+ export interface GraphIndexes {
21
+ /** label → set of node IDs */
22
+ labelIndex: Map<string, Set<string>>;
23
+ /** propertyKey → propertyValue → set of node IDs (values are string-coerced) */
24
+ propertyIndex: Map<string, Map<string, Set<string>>>;
25
+ /**
26
+ * Edge-type adjacency index.
27
+ * out[type][source] = set of { target, edgeId } for outgoing edges
28
+ * in[type][target] = set of { source, edgeId } for incoming edges
29
+ */
30
+ edgeTypeIndex: {
31
+ out: Map<string, Map<string, Array<{
32
+ target: string;
33
+ edgeId: string;
34
+ }>>>;
35
+ in: Map<string, Map<string, Array<{
36
+ source: string;
37
+ edgeId: string;
38
+ }>>>;
39
+ };
40
+ }
41
+ export type CypherValue = CypherNode | CypherEdge[] | CypherLiteral | null | undefined;
42
+ export type Direction = 'OUT' | 'IN' | 'UNDIRECTED';
43
+ export interface NodePattern {
44
+ variable: string;
45
+ label: string | undefined;
46
+ properties: Record<string, CypherLiteral> | undefined;
47
+ }
48
+ export interface RelationPattern {
49
+ variable: string | undefined;
50
+ type: string | undefined;
51
+ minDepth: number | undefined;
52
+ maxDepth: number | undefined;
53
+ direction: Direction;
54
+ }
55
+ export interface MatchClause {
56
+ optional: boolean;
57
+ hasChains: boolean;
58
+ sourcePattern: NodePattern;
59
+ relationPattern: RelationPattern;
60
+ targetPattern: NodePattern;
61
+ }
62
+ export interface CreateClause {
63
+ type: 'CREATE';
64
+ variable: string;
65
+ label: string | undefined;
66
+ properties: Record<string, CypherLiteral> | undefined;
67
+ }
68
+ export interface DeleteClause {
69
+ type: 'DELETE';
70
+ variable: string;
71
+ }
72
+ export interface SetClause {
73
+ type: 'SET';
74
+ variable: string;
75
+ property: string;
76
+ value: CypherLiteral;
77
+ }
78
+ export type WriteClause = CreateClause | DeleteClause | SetClause;
79
+ export interface PropertyAccessExpression {
80
+ type: 'PropertyAccess';
81
+ variable: string;
82
+ property: string | undefined;
83
+ }
84
+ export interface LiteralExpression {
85
+ type: 'Literal';
86
+ value: CypherLiteral;
87
+ }
88
+ export interface AggregationExpression {
89
+ type: 'Aggregation';
90
+ aggregationType: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
91
+ variable: string;
92
+ property: string | undefined;
93
+ }
94
+ export type Expression = PropertyAccessExpression | LiteralExpression | AggregationExpression;
95
+ export interface BinaryExpression {
96
+ type: 'BinaryExpression';
97
+ operator: '>' | '<' | '=' | 'CONTAINS';
98
+ left: Expression;
99
+ right: Expression;
100
+ }
101
+ export interface Projection {
102
+ expression: Expression;
103
+ alias: string;
104
+ }
105
+ export interface WithClause {
106
+ projections: Projection[];
107
+ where: BinaryExpression | undefined;
108
+ orderBy: OrderByItem[] | undefined;
109
+ skip: number | undefined;
110
+ limit: number | undefined;
111
+ }
112
+ export interface OrderByItem {
113
+ expression: Expression;
114
+ direction: 'ASC' | 'DESC';
115
+ }
116
+ export interface ReturnClause {
117
+ projections: Projection[];
118
+ orderBy: OrderByItem[] | undefined;
119
+ skip: number | undefined;
120
+ limit: number | undefined;
121
+ }
122
+ export type Stage = {
123
+ type: 'MATCH';
124
+ clause: MatchClause;
125
+ } | {
126
+ type: 'WITH';
127
+ clause: WithClause;
128
+ } | {
129
+ type: 'WRITE';
130
+ clause: WriteClause;
131
+ };
132
+ export interface AdvancedCypherAST {
133
+ type: 'Query';
134
+ stages: Stage[];
135
+ return: ReturnClause | undefined;
136
+ }
137
+ export type QueryContext = Record<string, CypherNode | CypherEdge[] | CypherLiteral | null | undefined>;
138
+ export type ResultRow = Record<string, CypherNode | CypherEdge[] | CypherLiteral | null | undefined>;
139
+ //# sourceMappingURL=cypher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cypher.d.ts","sourceRoot":"","sources":["../../src/types/cypher.ts"],"names":[],"mappings":"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"}
package/docs/404.md ADDED
@@ -0,0 +1,11 @@
1
+ ---
2
+ layout: default
3
+ title: 404 – Page Not Found
4
+ permalink: /404/
5
+ ---
6
+
7
+ # 404 – Page Not Found
8
+
9
+ The page you're looking for doesn't exist or has been moved.
10
+
11
+ <a href="{{ '/' | relative_url }}" class="btn btn-primary">Back to Home →</a>
@@ -0,0 +1,42 @@
1
+ title: gcyphrq
2
+ description: A Cypher graph query engine for in-memory graphs built on Graphology. Available as a CLI tool and Node.js library.
3
+ url: "https://plelevier.github.io"
4
+ baseurl: "/gcyphrq"
5
+ repository: "plelevier/gcyphrq"
6
+
7
+ permalink: pretty
8
+
9
+ markdown: kramdown
10
+ kramdown:
11
+ syntax_highlighter: rouge
12
+ syntax_highlighter_opts:
13
+ css_class: 'highlight'
14
+ span:
15
+ line_numbers: false
16
+ block:
17
+ line_numbers: false
18
+ start_line: 1
19
+
20
+ plugins: []
21
+
22
+ # Navigation
23
+ nav:
24
+ - title: Getting Started
25
+ url: /getting-started/
26
+ - title: CLI Reference
27
+ url: /cli/
28
+ - title: Library API
29
+ url: /library-api/
30
+ - title: Query Guide
31
+ url: /query-guide/
32
+ - title: Examples
33
+ url: /examples/
34
+ - title: Skill Guide
35
+ url: /skill/
36
+ - title: GitHub
37
+ url: https://github.com/plelevier/gcyphrq
38
+ external: true
39
+
40
+ # Site settings
41
+ version: 0.11.0
42
+ node_version: ">=20"
@@ -0,0 +1,6 @@
1
+ <footer class="site-footer">
2
+ <div class="container">
3
+ <p>{{ site.title }} is released under the <a href="https://github.com/{{ site.repository }}/blob/main/LICENSE">MIT License</a>.</p>
4
+ <p>Built with <a href="https://graphology.github.io/">Graphology</a> and <a href="https://www.antlr.org/">ANTLR4</a>.</p>
5
+ </div>
6
+ </footer>
@@ -0,0 +1,13 @@
1
+ <meta name="description" content="{{ page.description | default: site.description }}">
2
+ <link rel="icon" href="{{ '/assets/favicon.svg' | relative_url }}" type="image/svg+xml">
3
+
4
+ <!-- Open Graph -->
5
+ <meta property="og:title" content="{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}">
6
+ <meta property="og:description" content="{{ page.description | default: site.description }}">
7
+ <meta property="og:type" content="website">
8
+ <meta property="og:url" content="{{ page.url | absolute_url }}">
9
+
10
+ <!-- Twitter -->
11
+ <meta name="twitter:card" content="summary">
12
+ <meta name="twitter:title" content="{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}">
13
+ <meta name="twitter:description" content="{{ page.description | default: site.description }}">
@@ -0,0 +1,23 @@
1
+ <header class="site-header">
2
+ <div class="container">
3
+ <a href="{{ '/' | relative_url }}" class="logo">
4
+ <span class="logo-icon">◈</span>
5
+ <span class="logo-text">{{ site.title }} <small>v{{ site.version }}</small></span>
6
+ </a>
7
+ <nav class="main-nav">
8
+ <input type="checkbox" id="nav-toggle" class="nav-toggle">
9
+ <label for="nav-toggle" class="nav-toggle-label" aria-label="Toggle navigation">
10
+ <span></span><span></span><span></span>
11
+ </label>
12
+ <ul>
13
+ {% for item in site.nav %}
14
+ <li>
15
+ <a href="{{ item.url | relative_url }}" {% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %} class="{% if page.url == item.url | relative_url %}active{% endif %}">
16
+ {{ item.title }}{% if item.external %} <span class="external-icon">↗</span>{% endif %}
17
+ </a>
18
+ </li>
19
+ {% endfor %}
20
+ </ul>
21
+ </nav>
22
+ </div>
23
+ </header>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}</title>
7
+ <link rel="stylesheet" href="{{ '/assets/main.css' | relative_url }}">
8
+ {% include head.html %}
9
+ </head>
10
+ <body>
11
+ {% include nav.html %}
12
+ <main class="content">
13
+ <div class="container">
14
+ {{ content }}
15
+ </div>
16
+ </main>
17
+ {% include footer.html %}
18
+ <script src="{{ '/assets/cypher-highlight.js' | relative_url }}"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,171 @@
1
+ // Lightweight Cypher keyword highlighter + copy buttons for code blocks
2
+ document.addEventListener('DOMContentLoaded', () => {
3
+ // Highlight Cypher keywords
4
+ // Rouge wraps code blocks: div.language-cypher > div.highlight > pre.highlight > code
5
+ // We must only replace the <code> innerHTML to preserve the .highlight wrapper (background + CSS)
6
+ document.querySelectorAll('.language-cypher').forEach(el => {
7
+ const code = el.querySelector('code');
8
+ if (!code) return;
9
+ const text = code.textContent;
10
+ const tokens = tokenize(text);
11
+ code.innerHTML = tokens.map(t => {
12
+ if (t.type === 'keyword') return `<span class="cy-kw">${t.text}</span>`;
13
+ if (t.type === 'function') return `<span class="cy-fn">${t.text}</span>`;
14
+ if (t.type === 'label') return `<span class="cy-label">${t.text}</span>`;
15
+ if (t.type === 'reltype') return `<span class="cy-rel">${t.text}</span>`;
16
+ if (t.type === 'string') return `<span class="cy-str">${t.text}</span>`;
17
+ if (t.type === 'number') return `<span class="cy-num">${t.text}</span>`;
18
+ if (t.type === 'comment') return `<span class="cy-cmt">${t.text}</span>`;
19
+ return escapeHtml(t.text);
20
+ }).join('');
21
+ });
22
+
23
+ // Add copy button to every code block
24
+ // Target the outermost .highlight div (the one that is a direct child of .highlighter-rouge)
25
+ document.querySelectorAll('.highlighter-rouge > .highlight').forEach(block => {
26
+ const btn = document.createElement('button');
27
+ btn.className = 'code-copy-btn';
28
+ btn.type = 'button';
29
+ btn.title = 'Copy';
30
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
31
+
32
+ // Wrap block + btn in a relative container
33
+ const wrapper = document.createElement('div');
34
+ wrapper.className = 'code-block-wrapper';
35
+ block.parentNode.replaceChild(wrapper, block);
36
+ wrapper.appendChild(block);
37
+ wrapper.appendChild(btn);
38
+
39
+ btn.addEventListener('click', () => {
40
+ const code = block.querySelector('code') || block;
41
+ const text = code.textContent.trim();
42
+ if (navigator.clipboard && navigator.clipboard.writeText) {
43
+ navigator.clipboard.writeText(text).then(() => {
44
+ btn.classList.add('copied');
45
+ setTimeout(() => btn.classList.remove('copied'), 1500);
46
+ });
47
+ } else {
48
+ // Fallback for non-HTTPS (local dev)
49
+ const ta = document.createElement('textarea');
50
+ ta.value = text;
51
+ ta.style.position = 'fixed';
52
+ ta.style.opacity = '0';
53
+ document.body.appendChild(ta);
54
+ ta.select();
55
+ document.execCommand('copy');
56
+ document.body.removeChild(ta);
57
+ btn.classList.add('copied');
58
+ setTimeout(() => btn.classList.remove('copied'), 1500);
59
+ }
60
+ });
61
+ });
62
+ });
63
+
64
+ function tokenize(src) {
65
+ const tokens = [];
66
+ const keywords = new Set([
67
+ 'MATCH', 'OPTIONAL', 'UNWIND', 'RETURN', 'WITH', 'WHERE',
68
+ 'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'FOREACH',
69
+ 'ORDER', 'BY', 'ASC', 'DESC', 'SKIP', 'LIMIT',
70
+ 'ON', 'UPDATE',
71
+ 'AND', 'OR', 'XOR', 'NOT', 'IN', 'IS', 'NULL', 'TRUE', 'FALSE',
72
+ 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
73
+ 'STARTS', 'ENDS', 'CONTAINS',
74
+ ]);
75
+ const functions = new Set([
76
+ 'count', 'sum', 'min', 'max', 'avg', 'collect',
77
+ 'head', 'tail', 'size', 'id', 'labels', 'properties',
78
+ 'type', 'nodes', 'relationships',
79
+ 'toString', 'toInt', 'toFloat', 'toBoolean',
80
+ 'coalesce', 'exists',
81
+ ]);
82
+
83
+ let i = 0;
84
+ while (i < src.length) {
85
+ // Comments
86
+ if (src[i] === '/' && src[i + 1] === '/') {
87
+ let end = src.indexOf('\n', i);
88
+ if (end === -1) end = src.length;
89
+ tokens.push({ type: 'comment', text: src.slice(i, end) });
90
+ i = end;
91
+ continue;
92
+ }
93
+
94
+ // Strings (single or double quoted)
95
+ if (src[i] === "'" || src[i] === '"') {
96
+ const quote = src[i];
97
+ let j = i + 1;
98
+ while (j < src.length && src[j] !== quote) {
99
+ if (src[j] === '\\') j++; // skip escaped char
100
+ j++;
101
+ }
102
+ j++; // closing quote
103
+ tokens.push({ type: 'string', text: src.slice(i, j) });
104
+ i = j;
105
+ continue;
106
+ }
107
+
108
+ // Numbers
109
+ if (/[0-9]/.test(src[i]) && (i === 0 || /[\s(,<>!=+\-*\/]/.test(src[i - 1]))) {
110
+ let j = i;
111
+ while (j < src.length && /[0-9.]/.test(src[j])) j++;
112
+ tokens.push({ type: 'number', text: src.slice(i, j) });
113
+ i = j;
114
+ continue;
115
+ }
116
+
117
+ // Labels :Label
118
+ if (src[i] === ':' && i + 1 < src.length && /[A-Za-z_]/.test(src[i + 1])) {
119
+ let j = i + 1;
120
+ while (j < src.length && /[A-Za-z_0-9]/.test(src[j])) j++;
121
+ tokens.push({ type: 'label', text: src.slice(i, j) });
122
+ i = j;
123
+ continue;
124
+ }
125
+
126
+ // Relationship types -[TYPE]-> or :TYPE
127
+ if (src[i] === '-' && src[i + 1] === '[') {
128
+ // Find the relationship type
129
+ let j = i + 2;
130
+ // skip whitespace
131
+ while (j < src.length && /\s/.test(src[j])) j++;
132
+ if (j < src.length && (src[j] === '>' || src[j] === '<' || /[A-Za-z_]/.test(src[j]))) {
133
+ if (/[A-Za-z_]/.test(src[j])) {
134
+ let k = j;
135
+ while (k < src.length && /[A-Za-z_0-9]/.test(src[k])) k++;
136
+ tokens.push({ type: 'text', text: src.slice(i, j) });
137
+ tokens.push({ type: 'reltype', text: src.slice(j, k) });
138
+ i = k;
139
+ continue;
140
+ }
141
+ }
142
+ }
143
+
144
+ // Identifiers / keywords / functions
145
+ if (/[A-Za-z_]/.test(src[i])) {
146
+ let j = i;
147
+ while (j < src.length && /[A-Za-z_0-9]/.test(src[j])) j++;
148
+ const word = src.slice(i, j);
149
+ const upper = word.toUpperCase();
150
+ if (keywords.has(upper)) {
151
+ tokens.push({ type: 'keyword', text: word });
152
+ } else if (functions.has(word.toLowerCase()) && j < src.length && src[j] === '(') {
153
+ tokens.push({ type: 'function', text: word });
154
+ } else {
155
+ tokens.push({ type: 'text', text: word });
156
+ }
157
+ i = j;
158
+ continue;
159
+ }
160
+
161
+ // Everything else
162
+ tokens.push({ type: 'text', text: src[i] });
163
+ i++;
164
+ }
165
+
166
+ return tokens;
167
+ }
168
+
169
+ function escapeHtml(s) {
170
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
171
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="6" fill="#4f46e5"/>
3
+ <text x="16" y="23" text-anchor="middle" fill="#fff" font-family="sans-serif" font-size="20" font-weight="bold">◈</text>
4
+ </svg>