graphify-sf 0.3.2__tar.gz → 0.3.3__tar.gz

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.
Files changed (56) hide show
  1. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/PKG-INFO +1 -1
  2. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/flow.py +18 -7
  3. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/PKG-INFO +1 -1
  4. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/pyproject.toml +1 -1
  5. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_flow.py +116 -0
  6. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/LICENSE +0 -0
  7. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/README.md +0 -0
  8. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/__init__.py +0 -0
  9. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/__main__.py +0 -0
  10. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/_bundled_skill.md +0 -0
  11. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/analyze.py +0 -0
  12. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/build.py +0 -0
  13. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/cache.py +0 -0
  14. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/cluster.py +0 -0
  15. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/detect.py +0 -0
  16. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/export.py +0 -0
  17. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/__init__.py +0 -0
  18. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/_ids.py +0 -0
  19. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/agentforce.py +0 -0
  20. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/apex.py +0 -0
  21. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/aura.py +0 -0
  22. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/automation.py +0 -0
  23. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/config.py +0 -0
  24. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/doc.py +0 -0
  25. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/layout.py +0 -0
  26. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/lwc.py +0 -0
  27. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/object.py +0 -0
  28. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/profile.py +0 -0
  29. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/visualforce.py +0 -0
  30. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/llm.py +0 -0
  31. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/report.py +0 -0
  32. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/security.py +0 -0
  33. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/serve.py +0 -0
  34. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/validate.py +0 -0
  35. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/watch.py +0 -0
  36. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/SOURCES.txt +0 -0
  37. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/dependency_links.txt +0 -0
  38. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/entry_points.txt +0 -0
  39. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/requires.txt +0 -0
  40. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/top_level.txt +0 -0
  41. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/setup.cfg +0 -0
  42. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_analyze.py +0 -0
  43. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_build.py +0 -0
  44. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_cli.py +0 -0
  45. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_cluster.py +0 -0
  46. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect.py +0 -0
  47. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect_docs.py +0 -0
  48. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect_skip_dirs.py +0 -0
  49. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_agentforce.py +0 -0
  50. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_apex.py +0 -0
  51. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_config.py +0 -0
  52. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_doc.py +0 -0
  53. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_lwc.py +0 -0
  54. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_object.py +0 -0
  55. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_pipeline.py +0 -0
  56. {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_llm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphify-sf
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required.
5
5
  Author: Ray Kuo
6
6
  License-Expression: MIT
@@ -122,15 +122,26 @@ def extract_flow(path: Path) -> dict:
122
122
  if sub_name:
123
123
  edges.append(_make_edge(flow_nid, flow_id(sub_name), "invokes", "EXTRACTED", str_path))
124
124
 
125
- # Object record operations
126
- _record_tags = ("recordLookups", "recordCreates", "recordUpdates", "recordDeletes")
127
- seen_objects: set[str] = set()
128
- for tag in _record_tags:
125
+ # Object record operations. The relation stays "references" (a generic relation
126
+ # reused by many extractors — never change its semantics), but each edge carries an
127
+ # "operation" field (read/create/update/delete) so downstream can distinguish
128
+ # read-only access from writes. Dedup is by (object, operation) so a flow that both
129
+ # reads and updates the same object yields two edges, not one collapsed reference.
130
+ _record_op_tags = {
131
+ "recordLookups": "read",
132
+ "recordCreates": "create",
133
+ "recordUpdates": "update",
134
+ "recordDeletes": "delete",
135
+ }
136
+ seen_record_ops: set[tuple[str, str]] = set()
137
+ for tag, operation in _record_op_tags.items():
129
138
  for el in _find_all(root_el, tag, ns):
130
139
  obj = _find_text(el, "object", ns)
131
- if obj and obj not in seen_objects:
132
- seen_objects.add(obj)
133
- edges.append(_make_edge(flow_nid, object_id(obj), "references", "EXTRACTED", str_path))
140
+ if obj and (obj, operation) not in seen_record_ops:
141
+ seen_record_ops.add((obj, operation))
142
+ edge = _make_edge(flow_nid, object_id(obj), "references", "EXTRACTED", str_path)
143
+ edge["operation"] = operation
144
+ edges.append(edge)
134
145
 
135
146
  # Subflows
136
147
  for subflow in _find_all(root_el, "subflows", ns):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphify-sf
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required.
5
5
  Author: Ray Kuo
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphify-sf"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Turn any Salesforce SFDX project into a queryable knowledge graph — fully offline, no org connection required."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -324,6 +324,122 @@ def test_extract_flow_action_call_subflow_creates_invokes_edge(tmp_path):
324
324
  assert "subflow" in invokes_edges[0]["target"].lower()
325
325
 
326
326
 
327
+ # ---------------------------------------------------------------------------
328
+ # Flow → Object record operations (read/create/update/delete)
329
+ # ---------------------------------------------------------------------------
330
+
331
+ READ_AND_UPDATE_SAME_OBJECT_XML = f"""\
332
+ <?xml version="1.0" encoding="UTF-8"?>
333
+ <Flow {_NS}>
334
+ <processType>AutoLaunchedFlow</processType>
335
+ <recordLookups>
336
+ <name>GetOpp</name>
337
+ <object>Opportunity</object>
338
+ </recordLookups>
339
+ <recordUpdates>
340
+ <name>UpdateOpp</name>
341
+ <object>Opportunity</object>
342
+ </recordUpdates>
343
+ </Flow>
344
+ """
345
+
346
+ PURE_UPDATE_XML = f"""\
347
+ <?xml version="1.0" encoding="UTF-8"?>
348
+ <Flow {_NS}>
349
+ <processType>AutoLaunchedFlow</processType>
350
+ <recordUpdates>
351
+ <name>UpdateOpp</name>
352
+ <object>Opportunity</object>
353
+ </recordUpdates>
354
+ </Flow>
355
+ """
356
+
357
+ ALL_FOUR_OPS_XML = f"""\
358
+ <?xml version="1.0" encoding="UTF-8"?>
359
+ <Flow {_NS}>
360
+ <processType>AutoLaunchedFlow</processType>
361
+ <recordLookups>
362
+ <name>GetAcc</name>
363
+ <object>Account</object>
364
+ </recordLookups>
365
+ <recordCreates>
366
+ <name>MakeContact</name>
367
+ <object>Contact</object>
368
+ </recordCreates>
369
+ <recordUpdates>
370
+ <name>UpdateAcc</name>
371
+ <object>Account</object>
372
+ </recordUpdates>
373
+ <recordDeletes>
374
+ <name>DeleteLead</name>
375
+ <object>Lead</object>
376
+ </recordDeletes>
377
+ </Flow>
378
+ """
379
+
380
+
381
+ def test_extract_flow_read_and_update_yields_two_edges(tmp_path):
382
+ """A flow that both reads and updates the same object yields TWO edges
383
+ (operation=read and operation=update), not one collapsed reference."""
384
+ from graphify_sf.extract.flow import extract_flow
385
+
386
+ f = tmp_path / "OppReadWrite.flow-meta.xml"
387
+ f.write_text(READ_AND_UPDATE_SAME_OBJECT_XML)
388
+
389
+ result = extract_flow(f)
390
+ opp_edges = [
391
+ e for e in result["edges"] if e.get("relation") == "references" and "opportunity" in e["target"].lower()
392
+ ]
393
+ assert len(opp_edges) == 2, "read+update on same object must not be deduped to one edge"
394
+ ops = {e.get("operation") for e in opp_edges}
395
+ assert ops == {"read", "update"}
396
+
397
+
398
+ def test_extract_flow_pure_update_carries_operation(tmp_path):
399
+ """A recordUpdates element produces a references edge with operation='update'."""
400
+ from graphify_sf.extract.flow import extract_flow
401
+
402
+ f = tmp_path / "OppUpdate.flow-meta.xml"
403
+ f.write_text(PURE_UPDATE_XML)
404
+
405
+ result = extract_flow(f)
406
+ ref_edges = [e for e in result["edges"] if e.get("relation") == "references"]
407
+ assert len(ref_edges) == 1
408
+ assert ref_edges[0].get("operation") == "update"
409
+ assert "opportunity" in ref_edges[0]["target"].lower()
410
+
411
+
412
+ def test_extract_flow_record_ops_relation_unchanged(tmp_path):
413
+ """Regression: record-op edges keep relation='references' (semantics unchanged);
414
+ only the new 'operation' field distinguishes them."""
415
+ from graphify_sf.extract.flow import extract_flow
416
+
417
+ f = tmp_path / "OppReadWrite.flow-meta.xml"
418
+ f.write_text(READ_AND_UPDATE_SAME_OBJECT_XML)
419
+
420
+ result = extract_flow(f)
421
+ record_op_edges = [e for e in result["edges"] if e.get("operation") is not None]
422
+ assert record_op_edges, "expected at least one record-op edge"
423
+ for e in record_op_edges:
424
+ assert e["relation"] == "references"
425
+ assert e["confidence"] == "EXTRACTED"
426
+
427
+
428
+ def test_extract_flow_all_four_operations(tmp_path):
429
+ """All four record-op tags map to their operation: read/create/update/delete."""
430
+ from graphify_sf.extract.flow import extract_flow
431
+
432
+ f = tmp_path / "AllOps.flow-meta.xml"
433
+ f.write_text(ALL_FOUR_OPS_XML)
434
+
435
+ result = extract_flow(f)
436
+ by_op = {e.get("operation"): e["target"].lower() for e in result["edges"] if e.get("operation") is not None}
437
+ assert by_op.get("read") and "account" in by_op["read"]
438
+ assert by_op.get("create") and "contact" in by_op["create"]
439
+ assert by_op.get("update") and "account" in by_op["update"]
440
+ assert by_op.get("delete") and "lead" in by_op["delete"]
441
+
442
+
327
443
  # ---------------------------------------------------------------------------
328
444
  # Flow child element nodes (Decision, Screen, Loop, Assignment)
329
445
  # ---------------------------------------------------------------------------
File without changes
File without changes
File without changes