leangraph 1.1.4 → 1.1.8

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/dist/executor.js CHANGED
@@ -1,4 +1,11 @@
1
1
  // Query Executor - Full pipeline: Cypher → Parse → Translate → Execute → Format
2
+ /**
3
+ * Escape a string for safe interpolation inside a SQL string literal.
4
+ * Prevents SQL injection via backtick-quoted Cypher identifiers.
5
+ */
6
+ function escSqlStr(s) {
7
+ return (s || "").replace(/'/g, "''");
8
+ }
2
9
  import { parse, } from "./parser.js";
3
10
  import { Translator } from "./translator.js";
4
11
  import { HybridExecutor } from "./engine/hybrid-executor.js";
@@ -215,15 +222,20 @@ export class Executor {
215
222
  if (flags.hasWith || flags.hasReturn) {
216
223
  this.validateOrderByVariables(parseResult.query, params);
217
224
  }
225
+ // Maximum number of results to return (security limit)
226
+ const MAX_RESULTS_LIMIT = 10000;
218
227
  // Helper to return successful result
219
228
  const makeResult = (data) => {
220
229
  const endTime = performance.now();
230
+ const truncated = data.length > MAX_RESULTS_LIMIT;
231
+ const limitedData = truncated ? data.slice(0, MAX_RESULTS_LIMIT) : data;
221
232
  return {
222
233
  success: true,
223
- data,
234
+ data: limitedData,
224
235
  meta: {
225
- count: data.length,
236
+ count: limitedData.length,
226
237
  time_ms: Math.round((endTime - startTime) * 100) / 100,
238
+ truncated,
227
239
  },
228
240
  };
229
241
  };
@@ -1850,7 +1862,7 @@ export class Executor {
1850
1862
  }
1851
1863
  // Property conditions
1852
1864
  for (const [key, value] of Object.entries(props)) {
1853
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
1865
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
1854
1866
  conditionParams.push(value);
1855
1867
  }
1856
1868
  // Find existing nodes
@@ -1927,11 +1939,11 @@ export class Executor {
1927
1939
  for (const [key, value] of Object.entries(edgeProps)) {
1928
1940
  if (Array.isArray(value) || (typeof value === "object" && value !== null)) {
1929
1941
  // For arrays and objects, compare JSON representations
1930
- propConditions += ` AND json_extract(properties, '$.${key}') = json(?)`;
1942
+ propConditions += ` AND json_extract(properties, '$.${escSqlStr(key)}') = json(?)`;
1931
1943
  propParams.push(JSON.stringify(value));
1932
1944
  }
1933
1945
  else {
1934
- propConditions += ` AND json_extract(properties, '$.${key}') = ?`;
1946
+ propConditions += ` AND json_extract(properties, '$.${escSqlStr(key)}') = ?`;
1935
1947
  propParams.push(value);
1936
1948
  }
1937
1949
  }
@@ -3248,7 +3260,7 @@ export class Executor {
3248
3260
  continue;
3249
3261
  if (assignment.property && assignment.value) {
3250
3262
  const value = this.evaluateExpressionInRow(assignment.value, row, params);
3251
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
3263
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
3252
3264
  // Invalidate cache after UPDATE
3253
3265
  this.invalidatePropertyCache(nodeId);
3254
3266
  }
@@ -3308,12 +3320,12 @@ export class Executor {
3308
3320
  // Not valid JSON, treat as node ID
3309
3321
  }
3310
3322
  // Try as node ID
3311
- let result = this.db.execute(`SELECT json_extract(properties, '$.${expr.property}') as value FROM nodes WHERE id = ?`, [varValue]);
3323
+ let result = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(expr.property)}') as value FROM nodes WHERE id = ?`, [varValue]);
3312
3324
  if (result.rows.length > 0) {
3313
3325
  return this.deepParseJson(result.rows[0].value);
3314
3326
  }
3315
3327
  // Try edges
3316
- result = this.db.execute(`SELECT json_extract(properties, '$.${expr.property}') as value FROM edges WHERE id = ?`, [varValue]);
3328
+ result = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(expr.property)}') as value FROM edges WHERE id = ?`, [varValue]);
3317
3329
  if (result.rows.length > 0) {
3318
3330
  return this.deepParseJson(result.rows[0].value);
3319
3331
  }
@@ -4725,7 +4737,7 @@ export class Executor {
4725
4737
  whereParams.push(nodePattern.label, nodePattern.label, nodePattern.label);
4726
4738
  }
4727
4739
  for (const [key, value] of Object.entries(props)) {
4728
- whereConditions.push(`json_extract(properties, '$.${key}') = ?`);
4740
+ whereConditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
4729
4741
  whereParams.push(value);
4730
4742
  }
4731
4743
  const existsQuery = whereConditions.length > 0
@@ -6277,7 +6289,7 @@ export class Executor {
6277
6289
  conditionParams.push(...labelCondition.params);
6278
6290
  }
6279
6291
  for (const [key, value] of Object.entries(matchProps)) {
6280
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
6292
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
6281
6293
  conditionParams.push(value);
6282
6294
  }
6283
6295
  const sql = conditions.length > 0
@@ -6362,7 +6374,7 @@ export class Executor {
6362
6374
  conditionParams.push(...labelCondition.params);
6363
6375
  }
6364
6376
  for (const [key, value] of Object.entries(matchProps)) {
6365
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
6377
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
6366
6378
  conditionParams.push(value);
6367
6379
  }
6368
6380
  // Try to find existing node
@@ -6403,7 +6415,7 @@ export class Executor {
6403
6415
  if (!assignment.value || !assignment.property)
6404
6416
  continue;
6405
6417
  const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
6406
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
6418
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
6407
6419
  // Invalidate cache after UPDATE
6408
6420
  this.invalidatePropertyCache(nodeId);
6409
6421
  }
@@ -6454,7 +6466,7 @@ export class Executor {
6454
6466
  if (!assignment.value || !assignment.property)
6455
6467
  continue;
6456
6468
  const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
6457
- this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), edgeId]);
6469
+ this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), edgeId]);
6458
6470
  }
6459
6471
  }
6460
6472
  }
@@ -6738,7 +6750,7 @@ export class Executor {
6738
6750
  conditionParams.push(...labelCondition.params);
6739
6751
  }
6740
6752
  for (const [key, value] of Object.entries(matchProps)) {
6741
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
6753
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
6742
6754
  conditionParams.push(value);
6743
6755
  }
6744
6756
  const findSql = conditions.length > 0
@@ -6844,7 +6856,7 @@ export class Executor {
6844
6856
  conditionParams.push(...labelCondition.params);
6845
6857
  }
6846
6858
  for (const [key, value] of Object.entries(matchProps)) {
6847
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
6859
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
6848
6860
  conditionParams.push(value);
6849
6861
  }
6850
6862
  // Try to find existing node
@@ -6903,7 +6915,7 @@ export class Executor {
6903
6915
  if (!assignment.value || !assignment.property)
6904
6916
  continue;
6905
6917
  const value = this.evaluateExpressionWithMatchedNodes(assignment.value, params, matchedNodes);
6906
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
6918
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
6907
6919
  // Invalidate cache after UPDATE
6908
6920
  this.invalidatePropertyCache(nodeId);
6909
6921
  }
@@ -6986,7 +6998,7 @@ export class Executor {
6986
6998
  findEdgeParams.push(edgeType);
6987
6999
  }
6988
7000
  for (const [key, value] of Object.entries(edgeProps)) {
6989
- findEdgeConditions.push(`json_extract(properties, '$.${key}') = ?`);
7001
+ findEdgeConditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
6990
7002
  findEdgeParams.push(value);
6991
7003
  }
6992
7004
  const findEdgeSql = `SELECT id, type, source_id, target_id, properties FROM edges WHERE ${findEdgeConditions.join(" AND ")}`;
@@ -7025,7 +7037,7 @@ export class Executor {
7025
7037
  // Check if assignment is for the edge variable
7026
7038
  if (pattern.edge.variable && assignment.variable === pattern.edge.variable) {
7027
7039
  const value = this.evaluateExpression(assignment.value, params);
7028
- this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), edgeId]);
7040
+ this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), edgeId]);
7029
7041
  }
7030
7042
  }
7031
7043
  }
@@ -7170,7 +7182,7 @@ export class Executor {
7170
7182
  conditionParams.push(...labelCondition.params);
7171
7183
  }
7172
7184
  for (const [key, value] of Object.entries(matchProps)) {
7173
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
7185
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
7174
7186
  conditionParams.push(value);
7175
7187
  }
7176
7188
  // Try to find existing node
@@ -7249,7 +7261,7 @@ export class Executor {
7249
7261
  const value = assignment.value.type === "property" || assignment.value.type === "binary"
7250
7262
  ? this.evaluateExpressionWithContext(assignment.value, params, resolvedIds)
7251
7263
  : this.evaluateExpression(assignment.value, params);
7252
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
7264
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
7253
7265
  // Invalidate cache after UPDATE
7254
7266
  this.invalidatePropertyCache(nodeId);
7255
7267
  }
@@ -7337,7 +7349,7 @@ export class Executor {
7337
7349
  }
7338
7350
  // Add edge property conditions if any
7339
7351
  for (const [key, value] of Object.entries(edgeProps)) {
7340
- findEdgeConditions.push(`json_extract(properties, '$.${key}') = ?`);
7352
+ findEdgeConditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
7341
7353
  findEdgeParams.push(value);
7342
7354
  }
7343
7355
  const findEdgeSql = `SELECT id, type, source_id, target_id, properties FROM edges WHERE ${findEdgeConditions.join(" AND ")}`;
@@ -7360,7 +7372,7 @@ export class Executor {
7360
7372
  continue;
7361
7373
  const value = this.evaluateExpression(assignment.value, params);
7362
7374
  // Update target node with ON CREATE SET
7363
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
7375
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
7364
7376
  // Invalidate cache after UPDATE
7365
7377
  this.invalidatePropertyCache(targetNodeId);
7366
7378
  }
@@ -7379,7 +7391,7 @@ export class Executor {
7379
7391
  continue;
7380
7392
  const value = this.evaluateExpression(assignment.value, params);
7381
7393
  // Update target node with ON MATCH SET
7382
- this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
7394
+ this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), targetNodeId]);
7383
7395
  // Invalidate cache after UPDATE
7384
7396
  this.invalidatePropertyCache(targetNodeId);
7385
7397
  }
@@ -7430,7 +7442,7 @@ export class Executor {
7430
7442
  conditionParams.push(...labelCondition.params);
7431
7443
  }
7432
7444
  for (const [key, value] of Object.entries(props)) {
7433
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
7445
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
7434
7446
  conditionParams.push(value);
7435
7447
  }
7436
7448
  // If we have conditions, try to find existing node
@@ -7912,7 +7924,7 @@ export class Executor {
7912
7924
  if (assignment.property) {
7913
7925
  // SET n.prop = value
7914
7926
  const propName = assignment.property;
7915
- const sql = `UPDATE ${table} SET properties = json_set(properties, '$.${propName}', json(?)) WHERE id = ?`;
7927
+ const sql = `UPDATE ${table} SET properties = json_set(properties, '$.${escSqlStr(propName)}', json(?)) WHERE id = ?`;
7916
7928
  this.db.execute(sql, [JSON.stringify(newValue), nodeId]);
7917
7929
  // Update the row object so subsequent iterations see the new value
7918
7930
  target[propName] = newValue;
@@ -8939,13 +8951,13 @@ export class Executor {
8939
8951
  const nodeId = resolvedIds[variable];
8940
8952
  if (nodeId) {
8941
8953
  // Try nodes first
8942
- const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM nodes WHERE id = ?`, [nodeId]);
8954
+ const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(property)}') as value FROM nodes WHERE id = ?`, [nodeId]);
8943
8955
  if (nodeResult.rows.length > 0) {
8944
8956
  resultRow[alias] = this.deepParseJson(nodeResult.rows[0].value);
8945
8957
  }
8946
8958
  else {
8947
8959
  // Try edges
8948
- const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${property}') as value FROM edges WHERE id = ?`, [nodeId]);
8960
+ const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(property)}') as value FROM edges WHERE id = ?`, [nodeId]);
8949
8961
  if (edgeResult.rows.length > 0) {
8950
8962
  resultRow[alias] = this.deepParseJson(edgeResult.rows[0].value);
8951
8963
  }
@@ -9043,7 +9055,7 @@ export class Executor {
9043
9055
  const nodeId = resolvedIds[arg.variable];
9044
9056
  if (nodeId) {
9045
9057
  // Try nodes first
9046
- const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${arg.property}') as value FROM nodes WHERE id = ?`, [nodeId]);
9058
+ const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(arg.property)}') as value FROM nodes WHERE id = ?`, [nodeId]);
9047
9059
  if (nodeResult.rows.length > 0) {
9048
9060
  const value = this.deepParseJson(nodeResult.rows[0].value);
9049
9061
  if (Array.isArray(value)) {
@@ -9058,7 +9070,7 @@ export class Executor {
9058
9070
  }
9059
9071
  else {
9060
9072
  // Try edges
9061
- const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${arg.property}') as value FROM edges WHERE id = ?`, [nodeId]);
9073
+ const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(arg.property)}') as value FROM edges WHERE id = ?`, [nodeId]);
9062
9074
  if (edgeResult.rows.length > 0) {
9063
9075
  const value = this.deepParseJson(edgeResult.rows[0].value);
9064
9076
  if (Array.isArray(value)) {
@@ -9157,7 +9169,7 @@ export class Executor {
9157
9169
  }
9158
9170
  if (nullKeys.length > 0) {
9159
9171
  // Need to merge non-null props and remove null keys
9160
- const removePaths = nullKeys.map(k => `'$.${k}'`).join(', ');
9172
+ const removePaths = nullKeys.map(k => `'$.${escSqlStr(k)}'`).join(', ');
9161
9173
  const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_remove(json_patch(properties, ?), ${removePaths}) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
9162
9174
  if (nodeResult.changes === 0) {
9163
9175
  this.db.execute(`UPDATE edges SET properties = json_remove(json_patch(properties, ?), ${removePaths}) WHERE id = ?`, [JSON.stringify(nonNullProps), nodeId]);
@@ -9184,19 +9196,19 @@ export class Executor {
9184
9196
  : this.evaluateExpression(assignment.value, params);
9185
9197
  // If value is null, remove the property instead of setting it to null
9186
9198
  if (value === null) {
9187
- const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_remove(properties, '$.${assignment.property}') WHERE id = ?`, [nodeId]);
9199
+ const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_remove(properties, '$.${escSqlStr(assignment.property)}') WHERE id = ?`, [nodeId]);
9188
9200
  if (nodeResult.changes === 0) {
9189
- this.db.execute(`UPDATE edges SET properties = json_remove(properties, '$.${assignment.property}') WHERE id = ?`, [nodeId]);
9201
+ this.db.execute(`UPDATE edges SET properties = json_remove(properties, '$.${escSqlStr(assignment.property)}') WHERE id = ?`, [nodeId]);
9190
9202
  }
9191
9203
  }
9192
9204
  else {
9193
9205
  // Update the property using json_set
9194
9206
  // We need to determine if it's a node or edge - for now assume node
9195
9207
  // Try nodes first, then edges
9196
- const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
9208
+ const nodeResult = this.db.execute(`UPDATE nodes SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
9197
9209
  if (nodeResult.changes === 0) {
9198
9210
  // Try edges
9199
- this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
9211
+ this.db.execute(`UPDATE edges SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`, [JSON.stringify(value), nodeId]);
9200
9212
  }
9201
9213
  }
9202
9214
  // Invalidate cache after UPDATE
@@ -9317,7 +9329,7 @@ export class Executor {
9317
9329
  throw new Error(`Unknown variable: ${varName}`);
9318
9330
  }
9319
9331
  // Try nodes first
9320
- const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${propName}') AS value FROM nodes WHERE id = ?`, [entityId]);
9332
+ const nodeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(propName)}') AS value FROM nodes WHERE id = ?`, [entityId]);
9321
9333
  if (nodeResult.rows.length > 0) {
9322
9334
  const value = nodeResult.rows[0].value;
9323
9335
  // json_extract returns JSON-encoded strings for arrays/objects
@@ -9333,7 +9345,7 @@ export class Executor {
9333
9345
  return value;
9334
9346
  }
9335
9347
  // Try edges
9336
- const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${propName}') AS value FROM edges WHERE id = ?`, [entityId]);
9348
+ const edgeResult = this.db.execute(`SELECT json_extract(properties, '$.${escSqlStr(propName)}') AS value FROM edges WHERE id = ?`, [entityId]);
9337
9349
  if (edgeResult.rows.length > 0) {
9338
9350
  const value = edgeResult.rows[0].value;
9339
9351
  if (typeof value === "string" && (value.startsWith("[") || value.startsWith("{"))) {