flowquery 1.0.41 → 1.0.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "A declarative query language for data processing pipelines.",
5
5
  "main": "dist/index.node.js",
6
6
  "types": "dist/index.node.d.ts",
@@ -16,7 +16,11 @@ class NodeReference extends Node {
16
16
  return this._reference;
17
17
  }
18
18
  public async next(): Promise<void> {
19
- this.setValue(this._reference!.value()!);
19
+ const referenced = this._reference?.value();
20
+ if (referenced == null) {
21
+ return;
22
+ }
23
+ this.setValue(referenced);
20
24
  await this._outgoing?.find(this._value!.id);
21
25
  await this.runTodoNext();
22
26
  }
@@ -5,6 +5,7 @@ export type RelationshipMatchRecord = {
5
5
  startNode: Record<string, any>;
6
6
  endNode: Record<string, any> | null;
7
7
  properties: Record<string, any>;
8
+ [key: string]: any;
8
9
  };
9
10
 
10
11
  class RelationshipMatchCollector {
@@ -16,11 +17,13 @@ class RelationshipMatchCollector {
16
17
  const currentRecord = data?.current();
17
18
  const actualType =
18
19
  currentRecord && "_type" in currentRecord ? currentRecord["_type"] : relationship.type!;
20
+ const relProperties = data?.properties() as Record<string, any>;
19
21
  const match: RelationshipMatchRecord = {
22
+ ...(relProperties || {}),
20
23
  type: actualType,
21
24
  startNode: relationship.source?.value() || {},
22
25
  endNode: null,
23
- properties: data?.properties() as Record<string, any>,
26
+ properties: relProperties,
24
27
  };
25
28
  this._matches.push(match);
26
29
  this._nodeIds.push(traversalId);
@@ -3343,6 +3343,69 @@ test("Test match with ORed relationship types returns correct type in relationsh
3343
3343
  expect(results[1]).toEqual({ from: "NYC", to: "Chicago", type: "TRAIN" });
3344
3344
  });
3345
3345
 
3346
+ test("Test relationship properties can be accessed directly via dot notation", async () => {
3347
+ await new Runner(`
3348
+ CREATE VIRTUAL (:City) AS {
3349
+ unwind [
3350
+ {id: 1, name: 'NYC'},
3351
+ {id: 2, name: 'LA'},
3352
+ {id: 3, name: 'Chicago'}
3353
+ ] as record
3354
+ RETURN record.id as id, record.name as name
3355
+ }
3356
+ `).run();
3357
+ await new Runner(`
3358
+ CREATE VIRTUAL (:City)-[:FLIGHT]-(:City) AS {
3359
+ unwind [
3360
+ {left_id: 1, right_id: 2, airline: 'Delta', duration: 5}
3361
+ ] as record
3362
+ RETURN record.left_id as left_id, record.right_id as right_id, record.airline as airline, record.duration as duration
3363
+ }
3364
+ `).run();
3365
+ const match = new Runner(`
3366
+ MATCH (a:City)-[r:FLIGHT]->(b:City)
3367
+ RETURN a.name AS from, b.name AS to, r.airline AS airline, r.duration AS duration
3368
+ `);
3369
+ await match.run();
3370
+ const results = match.results;
3371
+ expect(results.length).toBe(1);
3372
+ expect(results[0]).toEqual({ from: "NYC", to: "LA", airline: "Delta", duration: 5 });
3373
+ });
3374
+
3375
+ test("Test relationship properties accessible via both direct access and properties()", async () => {
3376
+ await new Runner(`
3377
+ CREATE VIRTUAL (:Person) AS {
3378
+ unwind [
3379
+ {id: 1, name: 'Alice'},
3380
+ {id: 2, name: 'Bob'}
3381
+ ] as record
3382
+ RETURN record.id as id, record.name as name
3383
+ }
3384
+ `).run();
3385
+ await new Runner(`
3386
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
3387
+ unwind [
3388
+ {left_id: 1, right_id: 2, since: 2020, strength: 'strong'}
3389
+ ] as record
3390
+ RETURN record.left_id as left_id, record.right_id as right_id, record.since as since, record.strength as strength
3391
+ }
3392
+ `).run();
3393
+ const match = new Runner(`
3394
+ MATCH (a:Person)-[r:KNOWS]->(b:Person)
3395
+ RETURN a.name AS from, b.name AS to, r.since AS since, r.strength AS strength, properties(r).since AS propSince
3396
+ `);
3397
+ await match.run();
3398
+ const results = match.results;
3399
+ expect(results.length).toBe(1);
3400
+ expect(results[0]).toEqual({
3401
+ from: "Alice",
3402
+ to: "Bob",
3403
+ since: 2020,
3404
+ strength: "strong",
3405
+ propSince: 2020,
3406
+ });
3407
+ });
3408
+
3346
3409
  test("Test coalesce returns first non-null value", async () => {
3347
3410
  const runner = new Runner("RETURN coalesce(null, null, 'hello', 'world') as result");
3348
3411
  await runner.run();
@@ -4097,3 +4160,145 @@ test("Test RETURN alias shadowing graph variable in same RETURN clause", async (
4097
4160
  mentorDepartment: "Engineering",
4098
4161
  });
4099
4162
  });
4163
+
4164
+ test("Test chained optional match with null intermediate node", async () => {
4165
+ // Create a chain: A -> B -> C (C has no outgoing REPORTS_TO)
4166
+ await new Runner(`
4167
+ CREATE VIRTUAL (:Employee) AS {
4168
+ unwind [
4169
+ {id: 1, name: 'Alice'},
4170
+ {id: 2, name: 'Bob'},
4171
+ {id: 3, name: 'Charlie'}
4172
+ ] as record
4173
+ RETURN record.id as id, record.name as name
4174
+ }
4175
+ `).run();
4176
+ await new Runner(`
4177
+ CREATE VIRTUAL (:Employee)-[:REPORTS_TO]-(:Employee) AS {
4178
+ unwind [
4179
+ {left_id: 1, right_id: 2},
4180
+ {left_id: 2, right_id: 3}
4181
+ ] as record
4182
+ RETURN record.left_id as left_id, record.right_id as right_id
4183
+ }
4184
+ `).run();
4185
+
4186
+ // Alice -> Bob -> Charlie -> null -> null
4187
+ // m1=Bob, m2=Charlie, m3=null, m4=null (should not crash)
4188
+ const runner = new Runner(`
4189
+ MATCH (u:Employee)
4190
+ WHERE u.name = "Alice"
4191
+ OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:Employee)
4192
+ OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:Employee)
4193
+ OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:Employee)
4194
+ OPTIONAL MATCH (m3)-[:REPORTS_TO]->(m4:Employee)
4195
+ RETURN
4196
+ u.name AS user,
4197
+ m1.name AS manager1,
4198
+ m2.name AS manager2,
4199
+ m3.name AS manager3,
4200
+ m4.name AS manager4
4201
+ `);
4202
+ await runner.run();
4203
+ const results = runner.results;
4204
+
4205
+ expect(results.length).toBe(1);
4206
+ expect(results[0].user).toBe("Alice");
4207
+ expect(results[0].manager1).toBe("Bob");
4208
+ expect(results[0].manager2).toBe("Charlie");
4209
+ expect(results[0].manager3).toBeNull();
4210
+ expect(results[0].manager4).toBeNull();
4211
+ });
4212
+
4213
+ test("Test chained optional match all null from first optional", async () => {
4214
+ // Create nodes with no relationships
4215
+ await new Runner(`
4216
+ CREATE VIRTUAL (:Worker) AS {
4217
+ unwind [
4218
+ {id: 1, name: 'Solo'}
4219
+ ] as record
4220
+ RETURN record.id as id, record.name as name
4221
+ }
4222
+ `).run();
4223
+ await new Runner(`
4224
+ CREATE VIRTUAL (:Worker)-[:MANAGES]-(:Worker) AS {
4225
+ unwind [] as record
4226
+ RETURN record.left_id as left_id, record.right_id as right_id
4227
+ }
4228
+ `).run();
4229
+
4230
+ // Solo has no MANAGES relationship at all
4231
+ // m1=null, m2=null, m3=null
4232
+ const runner = new Runner(`
4233
+ MATCH (u:Worker)
4234
+ OPTIONAL MATCH (u)-[:MANAGES]->(m1:Worker)
4235
+ OPTIONAL MATCH (m1)-[:MANAGES]->(m2:Worker)
4236
+ OPTIONAL MATCH (m2)-[:MANAGES]->(m3:Worker)
4237
+ RETURN
4238
+ u.name AS user,
4239
+ m1.name AS mgr1,
4240
+ m2.name AS mgr2,
4241
+ m3.name AS mgr3
4242
+ `);
4243
+ await runner.run();
4244
+ const results = runner.results;
4245
+
4246
+ expect(results.length).toBe(1);
4247
+ expect(results[0].user).toBe("Solo");
4248
+ expect(results[0].mgr1).toBeNull();
4249
+ expect(results[0].mgr2).toBeNull();
4250
+ expect(results[0].mgr3).toBeNull();
4251
+ });
4252
+
4253
+ test("Test chained optional match with mixed null and non-null paths", async () => {
4254
+ // Two starting nodes: one with full chain, one with partial
4255
+ await new Runner(`
4256
+ CREATE VIRTUAL (:Staff) AS {
4257
+ unwind [
4258
+ {id: 1, name: 'Dev'},
4259
+ {id: 2, name: 'Lead'},
4260
+ {id: 3, name: 'Director'},
4261
+ {id: 4, name: 'Intern'}
4262
+ ] as record
4263
+ RETURN record.id as id, record.name as name
4264
+ }
4265
+ `).run();
4266
+ await new Runner(`
4267
+ CREATE VIRTUAL (:Staff)-[:REPORTS_TO]-(:Staff) AS {
4268
+ unwind [
4269
+ {left_id: 1, right_id: 2},
4270
+ {left_id: 2, right_id: 3}
4271
+ ] as record
4272
+ RETURN record.left_id as left_id, record.right_id as right_id
4273
+ }
4274
+ `).run();
4275
+
4276
+ // Dev -> Lead -> Director -> null
4277
+ // Intern -> null -> null -> null
4278
+ const runner = new Runner(`
4279
+ MATCH (u:Staff)
4280
+ WHERE u.name = "Dev" OR u.name = "Intern"
4281
+ OPTIONAL MATCH (u)-[:REPORTS_TO]->(m1:Staff)
4282
+ OPTIONAL MATCH (m1)-[:REPORTS_TO]->(m2:Staff)
4283
+ OPTIONAL MATCH (m2)-[:REPORTS_TO]->(m3:Staff)
4284
+ RETURN
4285
+ u.name AS user,
4286
+ m1.name AS mgr1,
4287
+ m2.name AS mgr2,
4288
+ m3.name AS mgr3
4289
+ `);
4290
+ await runner.run();
4291
+ const results = runner.results;
4292
+
4293
+ expect(results.length).toBe(2);
4294
+ // Dev's chain
4295
+ const dev = results.find((r: any) => r.user === "Dev");
4296
+ expect(dev.mgr1).toBe("Lead");
4297
+ expect(dev.mgr2).toBe("Director");
4298
+ expect(dev.mgr3).toBeNull();
4299
+ // Intern's chain
4300
+ const intern = results.find((r: any) => r.user === "Intern");
4301
+ expect(intern.mgr1).toBeNull();
4302
+ expect(intern.mgr2).toBeNull();
4303
+ expect(intern.mgr3).toBeNull();
4304
+ });