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.
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/PKG-INFO +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/flow.py +18 -7
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/PKG-INFO +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/pyproject.toml +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_flow.py +116 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/LICENSE +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/README.md +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/__init__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/__main__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/_bundled_skill.md +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/analyze.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/build.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/cache.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/cluster.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/detect.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/export.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/__init__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/_ids.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/agentforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/apex.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/aura.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/automation.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/config.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/doc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/layout.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/lwc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/object.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/profile.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/extract/visualforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/llm.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/report.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/security.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/serve.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/validate.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf/watch.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/SOURCES.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/dependency_links.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/entry_points.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/requires.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/graphify_sf.egg-info/top_level.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/setup.cfg +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_analyze.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_build.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_cli.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_cluster.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect_docs.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_detect_skip_dirs.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_agentforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_apex.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_config.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_doc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_lwc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_object.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_extract_pipeline.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.3}/tests/test_llm.py +0 -0
|
@@ -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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphify-sf"
|
|
7
|
-
version = "0.3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|