learngraph 0.1.1 → 0.3.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 +190 -21
- package/README.md +165 -3
- package/dist/cjs/llm/adapters/anthropic.js +124 -0
- package/dist/cjs/llm/adapters/anthropic.js.map +1 -0
- package/dist/cjs/llm/adapters/base.js +100 -0
- package/dist/cjs/llm/adapters/base.js.map +1 -0
- package/dist/cjs/llm/adapters/index.js +22 -0
- package/dist/cjs/llm/adapters/index.js.map +1 -0
- package/dist/cjs/llm/adapters/ollama.js +149 -0
- package/dist/cjs/llm/adapters/ollama.js.map +1 -0
- package/dist/cjs/llm/adapters/openai.js +126 -0
- package/dist/cjs/llm/adapters/openai.js.map +1 -0
- package/dist/cjs/llm/index.js +34 -5
- package/dist/cjs/llm/index.js.map +1 -1
- package/dist/cjs/llm/orchestrator.js +219 -0
- package/dist/cjs/llm/orchestrator.js.map +1 -0
- package/dist/cjs/llm/prompts.js +367 -0
- package/dist/cjs/llm/prompts.js.map +1 -0
- package/dist/cjs/parsers/base.js +189 -0
- package/dist/cjs/parsers/base.js.map +1 -0
- package/dist/cjs/parsers/demo.js +159 -0
- package/dist/cjs/parsers/demo.js.map +1 -0
- package/dist/cjs/parsers/extractor.js +191 -0
- package/dist/cjs/parsers/extractor.js.map +1 -0
- package/dist/cjs/parsers/index.js +43 -4
- package/dist/cjs/parsers/index.js.map +1 -1
- package/dist/cjs/parsers/json.js +157 -0
- package/dist/cjs/parsers/json.js.map +1 -0
- package/dist/cjs/parsers/markdown.js +168 -0
- package/dist/cjs/parsers/markdown.js.map +1 -0
- package/dist/cjs/parsers/samples.js +139 -0
- package/dist/cjs/parsers/samples.js.map +1 -0
- package/dist/cjs/storage/base.js +231 -0
- package/dist/cjs/storage/base.js.map +1 -0
- package/dist/cjs/storage/errors.js +128 -0
- package/dist/cjs/storage/errors.js.map +1 -0
- package/dist/cjs/storage/index.js +92 -5
- package/dist/cjs/storage/index.js.map +1 -1
- package/dist/cjs/storage/levelgraph.js +855 -0
- package/dist/cjs/storage/levelgraph.js.map +1 -0
- package/dist/cjs/storage/memory.js +447 -0
- package/dist/cjs/storage/memory.js.map +1 -0
- package/dist/cjs/storage/neo4j.js +866 -0
- package/dist/cjs/storage/neo4j.js.map +1 -0
- package/dist/cjs/storage/seeds.js +565 -0
- package/dist/cjs/storage/seeds.js.map +1 -0
- package/dist/cjs/types/llm.js +8 -0
- package/dist/cjs/types/llm.js.map +1 -0
- package/dist/cjs/types/parser.js +8 -0
- package/dist/cjs/types/parser.js.map +1 -0
- package/dist/esm/llm/adapters/anthropic.js +119 -0
- package/dist/esm/llm/adapters/anthropic.js.map +1 -0
- package/dist/esm/llm/adapters/base.js +95 -0
- package/dist/esm/llm/adapters/base.js.map +1 -0
- package/dist/esm/llm/adapters/index.js +10 -0
- package/dist/esm/llm/adapters/index.js.map +1 -0
- package/dist/esm/llm/adapters/ollama.js +144 -0
- package/dist/esm/llm/adapters/ollama.js.map +1 -0
- package/dist/esm/llm/adapters/openai.js +121 -0
- package/dist/esm/llm/adapters/openai.js.map +1 -0
- package/dist/esm/llm/index.js +12 -6
- package/dist/esm/llm/index.js.map +1 -1
- package/dist/esm/llm/orchestrator.js +214 -0
- package/dist/esm/llm/orchestrator.js.map +1 -0
- package/dist/esm/llm/prompts.js +360 -0
- package/dist/esm/llm/prompts.js.map +1 -0
- package/dist/esm/parsers/base.js +179 -0
- package/dist/esm/parsers/base.js.map +1 -0
- package/dist/esm/parsers/demo.js +154 -0
- package/dist/esm/parsers/demo.js.map +1 -0
- package/dist/esm/parsers/extractor.js +187 -0
- package/dist/esm/parsers/extractor.js.map +1 -0
- package/dist/esm/parsers/index.js +24 -5
- package/dist/esm/parsers/index.js.map +1 -1
- package/dist/esm/parsers/json.js +153 -0
- package/dist/esm/parsers/json.js.map +1 -0
- package/dist/esm/parsers/markdown.js +164 -0
- package/dist/esm/parsers/markdown.js.map +1 -0
- package/dist/esm/parsers/samples.js +136 -0
- package/dist/esm/parsers/samples.js.map +1 -0
- package/dist/esm/storage/base.js +221 -0
- package/dist/esm/storage/base.js.map +1 -0
- package/dist/esm/storage/errors.js +116 -0
- package/dist/esm/storage/errors.js.map +1 -0
- package/dist/esm/storage/index.js +71 -6
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/levelgraph.js +818 -0
- package/dist/esm/storage/levelgraph.js.map +1 -0
- package/dist/esm/storage/memory.js +443 -0
- package/dist/esm/storage/memory.js.map +1 -0
- package/dist/esm/storage/neo4j.js +829 -0
- package/dist/esm/storage/neo4j.js.map +1 -0
- package/dist/esm/storage/seeds.js +561 -0
- package/dist/esm/storage/seeds.js.map +1 -0
- package/dist/esm/types/llm.js +7 -0
- package/dist/esm/types/llm.js.map +1 -0
- package/dist/esm/types/parser.js +7 -0
- package/dist/esm/types/parser.js.map +1 -0
- package/dist/types/llm/adapters/anthropic.d.ts +21 -0
- package/dist/types/llm/adapters/anthropic.d.ts.map +1 -0
- package/dist/types/llm/adapters/base.d.ts +46 -0
- package/dist/types/llm/adapters/base.d.ts.map +1 -0
- package/dist/types/llm/adapters/index.d.ts +11 -0
- package/dist/types/llm/adapters/index.d.ts.map +1 -0
- package/dist/types/llm/adapters/ollama.d.ts +30 -0
- package/dist/types/llm/adapters/ollama.d.ts.map +1 -0
- package/dist/types/llm/adapters/openai.d.ts +22 -0
- package/dist/types/llm/adapters/openai.d.ts.map +1 -0
- package/dist/types/llm/index.d.ts +5 -0
- package/dist/types/llm/index.d.ts.map +1 -1
- package/dist/types/llm/orchestrator.d.ts +35 -0
- package/dist/types/llm/orchestrator.d.ts.map +1 -0
- package/dist/types/llm/prompts.d.ts +269 -0
- package/dist/types/llm/prompts.d.ts.map +1 -0
- package/dist/types/parsers/base.d.ts +39 -0
- package/dist/types/parsers/base.d.ts.map +1 -0
- package/dist/types/parsers/demo.d.ts +87 -0
- package/dist/types/parsers/demo.d.ts.map +1 -0
- package/dist/types/parsers/extractor.d.ts +43 -0
- package/dist/types/parsers/extractor.d.ts.map +1 -0
- package/dist/types/parsers/index.d.ts +10 -0
- package/dist/types/parsers/index.d.ts.map +1 -1
- package/dist/types/parsers/json.d.ts +71 -0
- package/dist/types/parsers/json.d.ts.map +1 -0
- package/dist/types/parsers/markdown.d.ts +43 -0
- package/dist/types/parsers/markdown.d.ts.map +1 -0
- package/dist/types/parsers/samples.d.ts +27 -0
- package/dist/types/parsers/samples.d.ts.map +1 -0
- package/dist/types/storage/base.d.ts +39 -0
- package/dist/types/storage/base.d.ts.map +1 -0
- package/dist/types/storage/errors.d.ts +74 -0
- package/dist/types/storage/errors.d.ts.map +1 -0
- package/dist/types/storage/index.d.ts +50 -2
- package/dist/types/storage/index.d.ts.map +1 -1
- package/dist/types/storage/levelgraph.d.ts +92 -0
- package/dist/types/storage/levelgraph.d.ts.map +1 -0
- package/dist/types/storage/memory.d.ts +70 -0
- package/dist/types/storage/memory.d.ts.map +1 -0
- package/dist/types/storage/neo4j.d.ts +88 -0
- package/dist/types/storage/neo4j.d.ts.map +1 -0
- package/dist/types/storage/seeds.d.ts +27 -0
- package/dist/types/storage/seeds.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +2 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/types/llm.d.ts +298 -0
- package/dist/types/types/llm.d.ts.map +1 -0
- package/dist/types/types/parser.d.ts +208 -0
- package/dist/types/types/parser.d.ts.map +1 -0
- package/package.json +4 -2
- package/scripts/postinstall.js +68 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Neo4j storage adapter
|
|
4
|
+
*
|
|
5
|
+
* Production-ready graph storage using Neo4j graph database
|
|
6
|
+
* with full Cypher query support and connection pooling.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.Neo4jStorage = void 0;
|
|
45
|
+
const index_js_1 = require("../types/index.js");
|
|
46
|
+
const errors_js_1 = require("./errors.js");
|
|
47
|
+
const base_js_1 = require("./base.js");
|
|
48
|
+
/**
|
|
49
|
+
* Neo4j storage adapter.
|
|
50
|
+
*
|
|
51
|
+
* Production-ready graph storage with full CRUD operations,
|
|
52
|
+
* connection pooling, and optimized Cypher queries.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { Neo4jStorage } from 'learngraph/storage';
|
|
57
|
+
*
|
|
58
|
+
* const storage = new Neo4jStorage();
|
|
59
|
+
* await storage.connect({
|
|
60
|
+
* backend: 'neo4j',
|
|
61
|
+
* uri: 'bolt://localhost:7687',
|
|
62
|
+
* username: 'neo4j',
|
|
63
|
+
* password: 'password',
|
|
64
|
+
* database: 'learngraph',
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Create a skill
|
|
68
|
+
* const skill = await storage.createSkill({
|
|
69
|
+
* name: 'Add Fractions',
|
|
70
|
+
* description: 'Add fractions with like denominators',
|
|
71
|
+
* bloomLevel: 'apply',
|
|
72
|
+
* difficulty: 0.4,
|
|
73
|
+
* isThresholdConcept: false,
|
|
74
|
+
* masteryThreshold: 0.8,
|
|
75
|
+
* estimatedMinutes: 30,
|
|
76
|
+
* tags: ['math', 'fractions'],
|
|
77
|
+
* metadata: {},
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* // Query the graph
|
|
81
|
+
* const prerequisites = await storage.getPrerequisitesOf(skill.id);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
class Neo4jStorage {
|
|
85
|
+
driver = null;
|
|
86
|
+
config = null;
|
|
87
|
+
_connected = false;
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Session Management
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
getSession() {
|
|
92
|
+
if (!this.driver || !this._connected) {
|
|
93
|
+
throw new errors_js_1.ConnectionError('Not connected to Neo4j');
|
|
94
|
+
}
|
|
95
|
+
return this.driver.session({
|
|
96
|
+
database: this.config?.database || 'neo4j',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async runQuery(query, params) {
|
|
100
|
+
const session = this.getSession();
|
|
101
|
+
try {
|
|
102
|
+
return await session.run(query, params);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await session.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Record Conversion
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
recordToSkill(record) {
|
|
112
|
+
const s = record;
|
|
113
|
+
const node = {
|
|
114
|
+
id: (0, index_js_1.createSkillId)(s.id),
|
|
115
|
+
name: s.name,
|
|
116
|
+
description: s.description,
|
|
117
|
+
bloomLevel: s.bloomLevel,
|
|
118
|
+
difficulty: s.difficulty,
|
|
119
|
+
isThresholdConcept: s.isThresholdConcept,
|
|
120
|
+
masteryThreshold: s.masteryThreshold,
|
|
121
|
+
estimatedMinutes: s.estimatedMinutes,
|
|
122
|
+
tags: s.tags,
|
|
123
|
+
metadata: typeof s.metadata === 'string' ? JSON.parse(s.metadata) : s.metadata || {},
|
|
124
|
+
createdAt: s.createdAt,
|
|
125
|
+
updatedAt: s.updatedAt,
|
|
126
|
+
};
|
|
127
|
+
// Only add optional fields if they have values
|
|
128
|
+
if (s.standardAlignment) {
|
|
129
|
+
node.standardAlignment = s.standardAlignment;
|
|
130
|
+
}
|
|
131
|
+
if (s.domain) {
|
|
132
|
+
node.domain = s.domain;
|
|
133
|
+
}
|
|
134
|
+
if (s.gradeLevel) {
|
|
135
|
+
node.gradeLevel = s.gradeLevel;
|
|
136
|
+
}
|
|
137
|
+
return node;
|
|
138
|
+
}
|
|
139
|
+
recordToEdge(record) {
|
|
140
|
+
const e = record;
|
|
141
|
+
const edge = {
|
|
142
|
+
id: (0, index_js_1.createEdgeId)(e.id),
|
|
143
|
+
sourceId: (0, index_js_1.createSkillId)(e.sourceId),
|
|
144
|
+
targetId: (0, index_js_1.createSkillId)(e.targetId),
|
|
145
|
+
strength: e.strength,
|
|
146
|
+
type: e.type,
|
|
147
|
+
metadata: typeof e.metadata === 'string' ? JSON.parse(e.metadata) : e.metadata || {},
|
|
148
|
+
createdAt: e.createdAt,
|
|
149
|
+
};
|
|
150
|
+
// Only add optional fields if they have values
|
|
151
|
+
if (e.reasoning) {
|
|
152
|
+
edge.reasoning = e.reasoning;
|
|
153
|
+
}
|
|
154
|
+
return edge;
|
|
155
|
+
}
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
// Connection Management
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
async connect(config) {
|
|
160
|
+
if (config.backend !== 'neo4j') {
|
|
161
|
+
throw new Error('Neo4jStorage only supports "neo4j" backend');
|
|
162
|
+
}
|
|
163
|
+
this.config = config;
|
|
164
|
+
try {
|
|
165
|
+
// Dynamic import of neo4j-driver
|
|
166
|
+
const neo4j = (await Promise.resolve().then(() => __importStar(require('neo4j-driver'))));
|
|
167
|
+
this.driver = neo4j.default.driver(this.config.uri, neo4j.default.auth.basic(this.config.username, this.config.password), {
|
|
168
|
+
maxConnectionPoolSize: this.config.maxConnectionPoolSize || 50,
|
|
169
|
+
connectionTimeout: this.config.connectionTimeout || 30000,
|
|
170
|
+
});
|
|
171
|
+
// Verify connectivity
|
|
172
|
+
await this.driver.verifyConnectivity();
|
|
173
|
+
// Initialize schema (constraints and indexes)
|
|
174
|
+
await this.initializeSchema();
|
|
175
|
+
this._connected = true;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new errors_js_1.ConnectionError(`Failed to connect to Neo4j: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async initializeSchema() {
|
|
182
|
+
const queries = [
|
|
183
|
+
// Skill node uniqueness constraint
|
|
184
|
+
`CREATE CONSTRAINT skill_id IF NOT EXISTS FOR (s:Skill) REQUIRE s.id IS UNIQUE`,
|
|
185
|
+
// Indexes for common queries
|
|
186
|
+
`CREATE INDEX skill_bloom_level IF NOT EXISTS FOR (s:Skill) ON (s.bloomLevel)`,
|
|
187
|
+
`CREATE INDEX skill_difficulty IF NOT EXISTS FOR (s:Skill) ON (s.difficulty)`,
|
|
188
|
+
`CREATE INDEX skill_threshold IF NOT EXISTS FOR (s:Skill) ON (s.isThresholdConcept)`,
|
|
189
|
+
`CREATE INDEX skill_domain IF NOT EXISTS FOR (s:Skill) ON (s.domain)`,
|
|
190
|
+
];
|
|
191
|
+
for (const query of queries) {
|
|
192
|
+
try {
|
|
193
|
+
await this.runQuery(query);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Ignore errors for already existing constraints/indexes
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async disconnect() {
|
|
201
|
+
if (this.driver) {
|
|
202
|
+
await this.driver.close();
|
|
203
|
+
this.driver = null;
|
|
204
|
+
}
|
|
205
|
+
this._connected = false;
|
|
206
|
+
}
|
|
207
|
+
isConnected() {
|
|
208
|
+
return this._connected;
|
|
209
|
+
}
|
|
210
|
+
async getStatus() {
|
|
211
|
+
const startTime = Date.now();
|
|
212
|
+
if (!this._connected || !this.driver) {
|
|
213
|
+
return {
|
|
214
|
+
connected: false,
|
|
215
|
+
backend: 'neo4j',
|
|
216
|
+
lastChecked: new Date(),
|
|
217
|
+
error: 'Not connected',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await this.driver.verifyConnectivity();
|
|
222
|
+
return {
|
|
223
|
+
connected: true,
|
|
224
|
+
backend: 'neo4j',
|
|
225
|
+
latencyMs: Date.now() - startTime,
|
|
226
|
+
lastChecked: new Date(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
connected: false,
|
|
232
|
+
backend: 'neo4j',
|
|
233
|
+
lastChecked: new Date(),
|
|
234
|
+
error: error instanceof Error ? error.message : String(error),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
// Skill Node CRUD
|
|
240
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
241
|
+
async createSkill(input) {
|
|
242
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
243
|
+
(0, base_js_1.validateSkillInput)(input);
|
|
244
|
+
const skill = (0, base_js_1.inputToSkillNode)(input);
|
|
245
|
+
const query = `
|
|
246
|
+
CREATE (s:Skill {
|
|
247
|
+
id: $id,
|
|
248
|
+
name: $name,
|
|
249
|
+
description: $description,
|
|
250
|
+
bloomLevel: $bloomLevel,
|
|
251
|
+
difficulty: $difficulty,
|
|
252
|
+
isThresholdConcept: $isThresholdConcept,
|
|
253
|
+
masteryThreshold: $masteryThreshold,
|
|
254
|
+
estimatedMinutes: $estimatedMinutes,
|
|
255
|
+
tags: $tags,
|
|
256
|
+
standardAlignment: $standardAlignment,
|
|
257
|
+
domain: $domain,
|
|
258
|
+
gradeLevel: $gradeLevel,
|
|
259
|
+
metadata: $metadata,
|
|
260
|
+
createdAt: $createdAt,
|
|
261
|
+
updatedAt: $updatedAt
|
|
262
|
+
})
|
|
263
|
+
RETURN s {.*} as skill
|
|
264
|
+
`;
|
|
265
|
+
try {
|
|
266
|
+
const result = await this.runQuery(query, {
|
|
267
|
+
id: skill.id,
|
|
268
|
+
name: skill.name,
|
|
269
|
+
description: skill.description,
|
|
270
|
+
bloomLevel: skill.bloomLevel,
|
|
271
|
+
difficulty: skill.difficulty,
|
|
272
|
+
isThresholdConcept: skill.isThresholdConcept,
|
|
273
|
+
masteryThreshold: skill.masteryThreshold,
|
|
274
|
+
estimatedMinutes: skill.estimatedMinutes,
|
|
275
|
+
tags: skill.tags,
|
|
276
|
+
standardAlignment: skill.standardAlignment || null,
|
|
277
|
+
domain: skill.domain || null,
|
|
278
|
+
gradeLevel: skill.gradeLevel || null,
|
|
279
|
+
metadata: JSON.stringify(skill.metadata),
|
|
280
|
+
createdAt: skill.createdAt,
|
|
281
|
+
updatedAt: skill.updatedAt,
|
|
282
|
+
});
|
|
283
|
+
if (result.records.length === 0) {
|
|
284
|
+
throw new Error('Failed to create skill');
|
|
285
|
+
}
|
|
286
|
+
return skill;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (error instanceof Error &&
|
|
290
|
+
error.message.includes('already exists')) {
|
|
291
|
+
throw new errors_js_1.DuplicateError('skill', skill.id);
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async getSkill(id) {
|
|
297
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
298
|
+
const query = `
|
|
299
|
+
MATCH (s:Skill {id: $id})
|
|
300
|
+
RETURN s {.*} as skill
|
|
301
|
+
`;
|
|
302
|
+
const result = await this.runQuery(query, { id: id });
|
|
303
|
+
const firstRecord = result.records[0];
|
|
304
|
+
if (!firstRecord) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return this.recordToSkill(firstRecord.get('skill'));
|
|
308
|
+
}
|
|
309
|
+
async getSkills(ids) {
|
|
310
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
311
|
+
if (ids.length === 0)
|
|
312
|
+
return [];
|
|
313
|
+
const query = `
|
|
314
|
+
MATCH (s:Skill)
|
|
315
|
+
WHERE s.id IN $ids
|
|
316
|
+
RETURN s {.*} as skill
|
|
317
|
+
`;
|
|
318
|
+
const result = await this.runQuery(query, {
|
|
319
|
+
ids: ids.map((id) => id),
|
|
320
|
+
});
|
|
321
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
322
|
+
}
|
|
323
|
+
async updateSkill(id, updates) {
|
|
324
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
325
|
+
const existing = await this.getSkill(id);
|
|
326
|
+
if (!existing) {
|
|
327
|
+
throw new errors_js_1.NotFoundError('skill', id);
|
|
328
|
+
}
|
|
329
|
+
const updated = {
|
|
330
|
+
...existing,
|
|
331
|
+
...updates,
|
|
332
|
+
id: existing.id,
|
|
333
|
+
createdAt: existing.createdAt,
|
|
334
|
+
updatedAt: (0, base_js_1.nowISO)(),
|
|
335
|
+
};
|
|
336
|
+
const query = `
|
|
337
|
+
MATCH (s:Skill {id: $id})
|
|
338
|
+
SET s.name = $name,
|
|
339
|
+
s.description = $description,
|
|
340
|
+
s.bloomLevel = $bloomLevel,
|
|
341
|
+
s.difficulty = $difficulty,
|
|
342
|
+
s.isThresholdConcept = $isThresholdConcept,
|
|
343
|
+
s.masteryThreshold = $masteryThreshold,
|
|
344
|
+
s.estimatedMinutes = $estimatedMinutes,
|
|
345
|
+
s.tags = $tags,
|
|
346
|
+
s.standardAlignment = $standardAlignment,
|
|
347
|
+
s.domain = $domain,
|
|
348
|
+
s.gradeLevel = $gradeLevel,
|
|
349
|
+
s.metadata = $metadata,
|
|
350
|
+
s.updatedAt = $updatedAt
|
|
351
|
+
RETURN s {.*} as skill
|
|
352
|
+
`;
|
|
353
|
+
await this.runQuery(query, {
|
|
354
|
+
id: updated.id,
|
|
355
|
+
name: updated.name,
|
|
356
|
+
description: updated.description,
|
|
357
|
+
bloomLevel: updated.bloomLevel,
|
|
358
|
+
difficulty: updated.difficulty,
|
|
359
|
+
isThresholdConcept: updated.isThresholdConcept,
|
|
360
|
+
masteryThreshold: updated.masteryThreshold,
|
|
361
|
+
estimatedMinutes: updated.estimatedMinutes,
|
|
362
|
+
tags: updated.tags,
|
|
363
|
+
standardAlignment: updated.standardAlignment || null,
|
|
364
|
+
domain: updated.domain || null,
|
|
365
|
+
gradeLevel: updated.gradeLevel || null,
|
|
366
|
+
metadata: JSON.stringify(updated.metadata),
|
|
367
|
+
updatedAt: updated.updatedAt,
|
|
368
|
+
});
|
|
369
|
+
return updated;
|
|
370
|
+
}
|
|
371
|
+
async deleteSkill(id) {
|
|
372
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
373
|
+
const query = `
|
|
374
|
+
MATCH (s:Skill {id: $id})
|
|
375
|
+
DETACH DELETE s
|
|
376
|
+
`;
|
|
377
|
+
await this.runQuery(query, { id: id });
|
|
378
|
+
}
|
|
379
|
+
async findSkills(query) {
|
|
380
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
381
|
+
let cypher = 'MATCH (s:Skill)';
|
|
382
|
+
const params = {};
|
|
383
|
+
const whereClauses = [];
|
|
384
|
+
// Build WHERE clauses from filters
|
|
385
|
+
if (query.filters) {
|
|
386
|
+
const filters = query.filters;
|
|
387
|
+
for (let i = 0; i < filters.length; i++) {
|
|
388
|
+
const filter = filters[i];
|
|
389
|
+
const paramName = `filter${i}`;
|
|
390
|
+
switch (filter.operator) {
|
|
391
|
+
case 'eq':
|
|
392
|
+
whereClauses.push(`s.${filter.field} = $${paramName}`);
|
|
393
|
+
params[paramName] = filter.value;
|
|
394
|
+
break;
|
|
395
|
+
case 'neq':
|
|
396
|
+
whereClauses.push(`s.${filter.field} <> $${paramName}`);
|
|
397
|
+
params[paramName] = filter.value;
|
|
398
|
+
break;
|
|
399
|
+
case 'gt':
|
|
400
|
+
whereClauses.push(`s.${filter.field} > $${paramName}`);
|
|
401
|
+
params[paramName] = filter.value;
|
|
402
|
+
break;
|
|
403
|
+
case 'gte':
|
|
404
|
+
whereClauses.push(`s.${filter.field} >= $${paramName}`);
|
|
405
|
+
params[paramName] = filter.value;
|
|
406
|
+
break;
|
|
407
|
+
case 'lt':
|
|
408
|
+
whereClauses.push(`s.${filter.field} < $${paramName}`);
|
|
409
|
+
params[paramName] = filter.value;
|
|
410
|
+
break;
|
|
411
|
+
case 'lte':
|
|
412
|
+
whereClauses.push(`s.${filter.field} <= $${paramName}`);
|
|
413
|
+
params[paramName] = filter.value;
|
|
414
|
+
break;
|
|
415
|
+
case 'in':
|
|
416
|
+
whereClauses.push(`s.${filter.field} IN $${paramName}`);
|
|
417
|
+
params[paramName] = filter.value;
|
|
418
|
+
break;
|
|
419
|
+
case 'contains':
|
|
420
|
+
if (filter.field === 'tags') {
|
|
421
|
+
whereClauses.push(`$${paramName} IN s.tags`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
whereClauses.push(`s.${filter.field} CONTAINS $${paramName}`);
|
|
425
|
+
}
|
|
426
|
+
params[paramName] = filter.value;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (whereClauses.length > 0) {
|
|
432
|
+
cypher += ' WHERE ' + whereClauses.join(' AND ');
|
|
433
|
+
}
|
|
434
|
+
cypher += ' RETURN s {.*} as skill';
|
|
435
|
+
// Add ORDER BY
|
|
436
|
+
if (query.sorting && query.sorting.length > 0) {
|
|
437
|
+
const orderClauses = query.sorting.map((sortItem) => `s.${sortItem.field} ${sortItem.direction.toUpperCase()}`);
|
|
438
|
+
cypher += ' ORDER BY ' + orderClauses.join(', ');
|
|
439
|
+
}
|
|
440
|
+
// Add pagination
|
|
441
|
+
if (query.pagination) {
|
|
442
|
+
if (query.pagination.offset) {
|
|
443
|
+
cypher += ` SKIP ${query.pagination.offset}`;
|
|
444
|
+
}
|
|
445
|
+
cypher += ` LIMIT ${query.pagination.limit || 100}`;
|
|
446
|
+
}
|
|
447
|
+
const result = await this.runQuery(cypher, params);
|
|
448
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
449
|
+
}
|
|
450
|
+
async countSkills(query) {
|
|
451
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
452
|
+
if (!query || !query.filters || query.filters.length === 0) {
|
|
453
|
+
const result = await this.runQuery('MATCH (s:Skill) RETURN count(s) as count');
|
|
454
|
+
const firstRecord = result.records[0];
|
|
455
|
+
return firstRecord ? firstRecord.get('count').low : 0;
|
|
456
|
+
}
|
|
457
|
+
const { pagination, ...queryWithoutPagination } = query;
|
|
458
|
+
const skills = await this.findSkills(queryWithoutPagination);
|
|
459
|
+
return skills.length;
|
|
460
|
+
}
|
|
461
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
462
|
+
// Prerequisite Edge CRUD
|
|
463
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
464
|
+
async createPrerequisite(input) {
|
|
465
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
466
|
+
(0, base_js_1.validateEdgeInput)(input);
|
|
467
|
+
// Verify source exists
|
|
468
|
+
const source = await this.getSkill(input.sourceId);
|
|
469
|
+
if (!source) {
|
|
470
|
+
throw new errors_js_1.ReferenceError(input.id || 'new', input.sourceId, 'source');
|
|
471
|
+
}
|
|
472
|
+
// Verify target exists
|
|
473
|
+
const target = await this.getSkill(input.targetId);
|
|
474
|
+
if (!target) {
|
|
475
|
+
throw new errors_js_1.ReferenceError(input.id || 'new', input.targetId, 'target');
|
|
476
|
+
}
|
|
477
|
+
const edge = (0, base_js_1.inputToEdge)(input);
|
|
478
|
+
const query = `
|
|
479
|
+
MATCH (source:Skill {id: $sourceId})
|
|
480
|
+
MATCH (target:Skill {id: $targetId})
|
|
481
|
+
CREATE (source)-[r:PREREQUISITE_OF {
|
|
482
|
+
id: $id,
|
|
483
|
+
strength: $strength,
|
|
484
|
+
type: $type,
|
|
485
|
+
reasoning: $reasoning,
|
|
486
|
+
metadata: $metadata,
|
|
487
|
+
createdAt: $createdAt
|
|
488
|
+
}]->(target)
|
|
489
|
+
RETURN r {.*, sourceId: source.id, targetId: target.id} as edge
|
|
490
|
+
`;
|
|
491
|
+
try {
|
|
492
|
+
await this.runQuery(query, {
|
|
493
|
+
id: edge.id,
|
|
494
|
+
sourceId: edge.sourceId,
|
|
495
|
+
targetId: edge.targetId,
|
|
496
|
+
strength: edge.strength,
|
|
497
|
+
type: edge.type,
|
|
498
|
+
reasoning: edge.reasoning || null,
|
|
499
|
+
metadata: JSON.stringify(edge.metadata),
|
|
500
|
+
createdAt: edge.createdAt,
|
|
501
|
+
});
|
|
502
|
+
return edge;
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
if (error instanceof Error &&
|
|
506
|
+
error.message.includes('already exists')) {
|
|
507
|
+
throw new errors_js_1.DuplicateError('edge', edge.id);
|
|
508
|
+
}
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async getPrerequisite(id) {
|
|
513
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
514
|
+
const query = `
|
|
515
|
+
MATCH (source:Skill)-[r:PREREQUISITE_OF {id: $id}]->(target:Skill)
|
|
516
|
+
RETURN r {.*, sourceId: source.id, targetId: target.id} as edge
|
|
517
|
+
`;
|
|
518
|
+
const result = await this.runQuery(query, { id: id });
|
|
519
|
+
const firstRecord = result.records[0];
|
|
520
|
+
if (!firstRecord) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
return this.recordToEdge(firstRecord.get('edge'));
|
|
524
|
+
}
|
|
525
|
+
async deletePrerequisite(id) {
|
|
526
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
527
|
+
const query = `
|
|
528
|
+
MATCH ()-[r:PREREQUISITE_OF {id: $id}]->()
|
|
529
|
+
DELETE r
|
|
530
|
+
`;
|
|
531
|
+
await this.runQuery(query, { id: id });
|
|
532
|
+
}
|
|
533
|
+
async findPrerequisites(criteria) {
|
|
534
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
535
|
+
let query = 'MATCH (source:Skill)-[r:PREREQUISITE_OF]->(target:Skill)';
|
|
536
|
+
const whereClauses = [];
|
|
537
|
+
const params = {};
|
|
538
|
+
if (criteria.sourceId) {
|
|
539
|
+
whereClauses.push('source.id = $sourceId');
|
|
540
|
+
params.sourceId = criteria.sourceId;
|
|
541
|
+
}
|
|
542
|
+
if (criteria.targetId) {
|
|
543
|
+
whereClauses.push('target.id = $targetId');
|
|
544
|
+
params.targetId = criteria.targetId;
|
|
545
|
+
}
|
|
546
|
+
if (criteria.type) {
|
|
547
|
+
whereClauses.push('r.type = $type');
|
|
548
|
+
params.type = criteria.type;
|
|
549
|
+
}
|
|
550
|
+
if (criteria.minStrength !== undefined) {
|
|
551
|
+
whereClauses.push('r.strength >= $minStrength');
|
|
552
|
+
params.minStrength = criteria.minStrength;
|
|
553
|
+
}
|
|
554
|
+
if (whereClauses.length > 0) {
|
|
555
|
+
query += ' WHERE ' + whereClauses.join(' AND ');
|
|
556
|
+
}
|
|
557
|
+
query += ' RETURN r {.*, sourceId: source.id, targetId: target.id} as edge';
|
|
558
|
+
const result = await this.runQuery(query, params);
|
|
559
|
+
return result.records.map((record) => this.recordToEdge(record.get('edge')));
|
|
560
|
+
}
|
|
561
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
562
|
+
// Graph Traversal
|
|
563
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
564
|
+
async getPrerequisitesOf(skillId) {
|
|
565
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
566
|
+
const query = `
|
|
567
|
+
MATCH (prereq:Skill)-[:PREREQUISITE_OF]->(s:Skill {id: $id})
|
|
568
|
+
RETURN prereq {.*} as skill
|
|
569
|
+
`;
|
|
570
|
+
const result = await this.runQuery(query, { id: skillId });
|
|
571
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
572
|
+
}
|
|
573
|
+
async getDependentsOf(skillId) {
|
|
574
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
575
|
+
const query = `
|
|
576
|
+
MATCH (s:Skill {id: $id})-[:PREREQUISITE_OF]->(dependent:Skill)
|
|
577
|
+
RETURN dependent {.*} as skill
|
|
578
|
+
`;
|
|
579
|
+
const result = await this.runQuery(query, { id: skillId });
|
|
580
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
581
|
+
}
|
|
582
|
+
async getSubgraph(rootId, depth) {
|
|
583
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
584
|
+
// Get all nodes within depth
|
|
585
|
+
const nodeQuery = `
|
|
586
|
+
MATCH path = (prereq:Skill)-[:PREREQUISITE_OF*0..${depth}]->(root:Skill {id: $id})
|
|
587
|
+
UNWIND nodes(path) as n
|
|
588
|
+
WITH DISTINCT n
|
|
589
|
+
RETURN n {.*} as skill
|
|
590
|
+
`;
|
|
591
|
+
const nodeResult = await this.runQuery(nodeQuery, { id: rootId });
|
|
592
|
+
const nodes = nodeResult.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
593
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
594
|
+
// Get edges between these nodes
|
|
595
|
+
const edgeQuery = `
|
|
596
|
+
MATCH (source:Skill)-[r:PREREQUISITE_OF]->(target:Skill)
|
|
597
|
+
WHERE source.id IN $nodeIds AND target.id IN $nodeIds
|
|
598
|
+
RETURN r {.*, sourceId: source.id, targetId: target.id} as edge
|
|
599
|
+
`;
|
|
600
|
+
const edgeResult = await this.runQuery(edgeQuery, {
|
|
601
|
+
nodeIds: Array.from(nodeIds),
|
|
602
|
+
});
|
|
603
|
+
const edges = edgeResult.records.map((record) => this.recordToEdge(record.get('edge')));
|
|
604
|
+
return {
|
|
605
|
+
rootId,
|
|
606
|
+
depth,
|
|
607
|
+
nodes,
|
|
608
|
+
edges,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async getRootSkills() {
|
|
612
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
613
|
+
const query = `
|
|
614
|
+
MATCH (s:Skill)
|
|
615
|
+
WHERE NOT (:Skill)-[:PREREQUISITE_OF]->(s)
|
|
616
|
+
RETURN s {.*} as skill
|
|
617
|
+
`;
|
|
618
|
+
const result = await this.runQuery(query);
|
|
619
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
620
|
+
}
|
|
621
|
+
async getLeafSkills() {
|
|
622
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
623
|
+
const query = `
|
|
624
|
+
MATCH (s:Skill)
|
|
625
|
+
WHERE NOT (s)-[:PREREQUISITE_OF]->(:Skill)
|
|
626
|
+
RETURN s {.*} as skill
|
|
627
|
+
`;
|
|
628
|
+
const result = await this.runQuery(query);
|
|
629
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
630
|
+
}
|
|
631
|
+
async getPath(fromId, toId) {
|
|
632
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
633
|
+
const query = `
|
|
634
|
+
MATCH path = shortestPath(
|
|
635
|
+
(from:Skill {id: $fromId})-[:PREREQUISITE_OF*]->(to:Skill {id: $toId})
|
|
636
|
+
)
|
|
637
|
+
UNWIND nodes(path) as n
|
|
638
|
+
RETURN n {.*} as skill
|
|
639
|
+
`;
|
|
640
|
+
try {
|
|
641
|
+
const result = await this.runQuery(query, {
|
|
642
|
+
fromId: fromId,
|
|
643
|
+
toId: toId,
|
|
644
|
+
});
|
|
645
|
+
if (result.records.length === 0) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return result.records.map((record) => this.recordToSkill(record.get('skill')));
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
655
|
+
// Bulk Operations
|
|
656
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
657
|
+
async importGraph(nodes, edges, options) {
|
|
658
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
659
|
+
const startTime = Date.now();
|
|
660
|
+
const result = {
|
|
661
|
+
nodesCreated: 0,
|
|
662
|
+
edgesCreated: 0,
|
|
663
|
+
nodesSkipped: 0,
|
|
664
|
+
edgesSkipped: 0,
|
|
665
|
+
errors: [],
|
|
666
|
+
durationMs: 0,
|
|
667
|
+
};
|
|
668
|
+
if (options?.clearExisting) {
|
|
669
|
+
await this.clearAll();
|
|
670
|
+
}
|
|
671
|
+
// Batch import nodes using UNWIND for efficiency
|
|
672
|
+
const batchSize = 1000;
|
|
673
|
+
for (let i = 0; i < nodes.length; i += batchSize) {
|
|
674
|
+
const batch = nodes.slice(i, i + batchSize);
|
|
675
|
+
const skills = batch.map((input) => (0, base_js_1.inputToSkillNode)(input));
|
|
676
|
+
const query = `
|
|
677
|
+
UNWIND $skills as skill
|
|
678
|
+
MERGE (s:Skill {id: skill.id})
|
|
679
|
+
ON CREATE SET
|
|
680
|
+
s.name = skill.name,
|
|
681
|
+
s.description = skill.description,
|
|
682
|
+
s.bloomLevel = skill.bloomLevel,
|
|
683
|
+
s.difficulty = skill.difficulty,
|
|
684
|
+
s.isThresholdConcept = skill.isThresholdConcept,
|
|
685
|
+
s.masteryThreshold = skill.masteryThreshold,
|
|
686
|
+
s.estimatedMinutes = skill.estimatedMinutes,
|
|
687
|
+
s.tags = skill.tags,
|
|
688
|
+
s.standardAlignment = skill.standardAlignment,
|
|
689
|
+
s.domain = skill.domain,
|
|
690
|
+
s.gradeLevel = skill.gradeLevel,
|
|
691
|
+
s.metadata = skill.metadata,
|
|
692
|
+
s.createdAt = skill.createdAt,
|
|
693
|
+
s.updatedAt = skill.updatedAt
|
|
694
|
+
RETURN count(s) as count
|
|
695
|
+
`;
|
|
696
|
+
try {
|
|
697
|
+
const importResult = await this.runQuery(query, {
|
|
698
|
+
skills: skills.map((s) => ({
|
|
699
|
+
id: s.id,
|
|
700
|
+
name: s.name,
|
|
701
|
+
description: s.description,
|
|
702
|
+
bloomLevel: s.bloomLevel,
|
|
703
|
+
difficulty: s.difficulty,
|
|
704
|
+
isThresholdConcept: s.isThresholdConcept,
|
|
705
|
+
masteryThreshold: s.masteryThreshold,
|
|
706
|
+
estimatedMinutes: s.estimatedMinutes,
|
|
707
|
+
tags: s.tags,
|
|
708
|
+
standardAlignment: s.standardAlignment || null,
|
|
709
|
+
domain: s.domain || null,
|
|
710
|
+
gradeLevel: s.gradeLevel || null,
|
|
711
|
+
metadata: JSON.stringify(s.metadata),
|
|
712
|
+
createdAt: s.createdAt,
|
|
713
|
+
updatedAt: s.updatedAt,
|
|
714
|
+
})),
|
|
715
|
+
});
|
|
716
|
+
result.nodesCreated += importResult.summary.counters.nodesCreated();
|
|
717
|
+
result.nodesSkipped += batch.length - importResult.summary.counters.nodesCreated();
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
for (const input of batch) {
|
|
721
|
+
result.errors.push({
|
|
722
|
+
type: 'node',
|
|
723
|
+
id: input.id || 'unknown',
|
|
724
|
+
error: error instanceof Error ? error.message : String(error),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Batch import edges
|
|
730
|
+
for (let i = 0; i < edges.length; i += batchSize) {
|
|
731
|
+
const batch = edges.slice(i, i + batchSize);
|
|
732
|
+
const edgeData = batch.map((input) => (0, base_js_1.inputToEdge)(input));
|
|
733
|
+
const query = `
|
|
734
|
+
UNWIND $edges as edge
|
|
735
|
+
MATCH (source:Skill {id: edge.sourceId})
|
|
736
|
+
MATCH (target:Skill {id: edge.targetId})
|
|
737
|
+
MERGE (source)-[r:PREREQUISITE_OF {id: edge.id}]->(target)
|
|
738
|
+
ON CREATE SET
|
|
739
|
+
r.strength = edge.strength,
|
|
740
|
+
r.type = edge.type,
|
|
741
|
+
r.reasoning = edge.reasoning,
|
|
742
|
+
r.metadata = edge.metadata,
|
|
743
|
+
r.createdAt = edge.createdAt
|
|
744
|
+
RETURN count(r) as count
|
|
745
|
+
`;
|
|
746
|
+
try {
|
|
747
|
+
const importResult = await this.runQuery(query, {
|
|
748
|
+
edges: edgeData.map((e) => ({
|
|
749
|
+
id: e.id,
|
|
750
|
+
sourceId: e.sourceId,
|
|
751
|
+
targetId: e.targetId,
|
|
752
|
+
strength: e.strength,
|
|
753
|
+
type: e.type,
|
|
754
|
+
reasoning: e.reasoning || null,
|
|
755
|
+
metadata: JSON.stringify(e.metadata),
|
|
756
|
+
createdAt: e.createdAt,
|
|
757
|
+
})),
|
|
758
|
+
});
|
|
759
|
+
result.edgesCreated += importResult.summary.counters.relationshipsCreated();
|
|
760
|
+
result.edgesSkipped += batch.length - importResult.summary.counters.relationshipsCreated();
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
for (const input of batch) {
|
|
764
|
+
result.errors.push({
|
|
765
|
+
type: 'edge',
|
|
766
|
+
id: input.id || 'unknown',
|
|
767
|
+
error: error instanceof Error ? error.message : String(error),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
result.durationMs = Date.now() - startTime;
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
async exportGraph() {
|
|
776
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
777
|
+
const nodes = await this.findSkills({});
|
|
778
|
+
const edges = await this.findPrerequisites({});
|
|
779
|
+
return {
|
|
780
|
+
version: index_js_1.GRAPH_VERSION,
|
|
781
|
+
exportedAt: (0, base_js_1.nowISO)(),
|
|
782
|
+
nodes,
|
|
783
|
+
edges,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
async clearAll() {
|
|
787
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
788
|
+
const query = `
|
|
789
|
+
MATCH (s:Skill)
|
|
790
|
+
DETACH DELETE s
|
|
791
|
+
`;
|
|
792
|
+
await this.runQuery(query);
|
|
793
|
+
}
|
|
794
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
795
|
+
// Analytics
|
|
796
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
797
|
+
async getStats() {
|
|
798
|
+
(0, base_js_1.requireConnection)(this._connected);
|
|
799
|
+
// Get basic counts
|
|
800
|
+
const countQuery = `
|
|
801
|
+
MATCH (s:Skill)
|
|
802
|
+
OPTIONAL MATCH ()-[r:PREREQUISITE_OF]->()
|
|
803
|
+
WITH
|
|
804
|
+
count(DISTINCT s) as nodeCount,
|
|
805
|
+
count(DISTINCT r) as edgeCount,
|
|
806
|
+
count(DISTINCT CASE WHEN s.isThresholdConcept = true THEN s END) as thresholdCount
|
|
807
|
+
RETURN nodeCount, edgeCount, thresholdCount
|
|
808
|
+
`;
|
|
809
|
+
const countResult = await this.runQuery(countQuery);
|
|
810
|
+
const countsRecord = countResult.records[0];
|
|
811
|
+
// Get root and leaf counts
|
|
812
|
+
const rootQuery = `
|
|
813
|
+
MATCH (s:Skill)
|
|
814
|
+
WHERE NOT (:Skill)-[:PREREQUISITE_OF]->(s)
|
|
815
|
+
RETURN count(s) as count
|
|
816
|
+
`;
|
|
817
|
+
const rootResult = await this.runQuery(rootQuery);
|
|
818
|
+
const rootRecord = rootResult.records[0];
|
|
819
|
+
const leafQuery = `
|
|
820
|
+
MATCH (s:Skill)
|
|
821
|
+
WHERE NOT (s)-[:PREREQUISITE_OF]->(:Skill)
|
|
822
|
+
RETURN count(s) as count
|
|
823
|
+
`;
|
|
824
|
+
const leafResult = await this.runQuery(leafQuery);
|
|
825
|
+
const leafRecord = leafResult.records[0];
|
|
826
|
+
// Get bloom distribution
|
|
827
|
+
const bloomQuery = `
|
|
828
|
+
MATCH (s:Skill)
|
|
829
|
+
RETURN s.bloomLevel as level, count(s) as count
|
|
830
|
+
`;
|
|
831
|
+
const bloomResult = await this.runQuery(bloomQuery);
|
|
832
|
+
const bloomDistribution = {
|
|
833
|
+
remember: 0,
|
|
834
|
+
understand: 0,
|
|
835
|
+
apply: 0,
|
|
836
|
+
analyze: 0,
|
|
837
|
+
evaluate: 0,
|
|
838
|
+
create: 0,
|
|
839
|
+
};
|
|
840
|
+
for (const record of bloomResult.records) {
|
|
841
|
+
const level = record.get('level');
|
|
842
|
+
const count = record.get('count').low;
|
|
843
|
+
if (level && bloomDistribution.hasOwnProperty(level)) {
|
|
844
|
+
bloomDistribution[level] = count;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Calculate from actual data for remaining stats
|
|
848
|
+
const nodes = await this.findSkills({});
|
|
849
|
+
const edges = await this.findPrerequisites({});
|
|
850
|
+
const fullStats = (0, base_js_1.calculateStats)(nodes, edges);
|
|
851
|
+
return {
|
|
852
|
+
nodeCount: countsRecord ? countsRecord.get('nodeCount').low : 0,
|
|
853
|
+
edgeCount: countsRecord ? countsRecord.get('edgeCount').low : 0,
|
|
854
|
+
thresholdConceptCount: countsRecord ? countsRecord.get('thresholdCount').low : 0,
|
|
855
|
+
rootNodeCount: rootRecord ? rootRecord.get('count').low : 0,
|
|
856
|
+
leafNodeCount: leafRecord ? leafRecord.get('count').low : 0,
|
|
857
|
+
maxDepth: fullStats.maxDepth,
|
|
858
|
+
avgPrerequisites: fullStats.avgPrerequisites,
|
|
859
|
+
avgDependents: fullStats.avgDependents,
|
|
860
|
+
bloomDistribution,
|
|
861
|
+
difficultyDistribution: fullStats.difficultyDistribution,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
exports.Neo4jStorage = Neo4jStorage;
|
|
866
|
+
//# sourceMappingURL=neo4j.js.map
|