graphify-sf 0.3.2__tar.gz → 0.3.4__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.4}/PKG-INFO +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/apex.py +29 -13
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/flow.py +18 -7
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/PKG-INFO +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/pyproject.toml +1 -1
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_apex.py +107 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_flow.py +116 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/LICENSE +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/README.md +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/__init__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/__main__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/_bundled_skill.md +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/analyze.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/build.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/cache.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/cluster.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/detect.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/export.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/__init__.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/_ids.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/agentforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/aura.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/automation.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/config.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/doc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/layout.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/lwc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/object.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/profile.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/extract/visualforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/llm.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/report.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/security.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/serve.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/validate.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf/watch.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/SOURCES.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/dependency_links.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/entry_points.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/requires.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/graphify_sf.egg-info/top_level.txt +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/setup.cfg +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_analyze.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_build.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_cli.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_cluster.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_detect.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_detect_docs.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_detect_skip_dirs.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_agentforce.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_config.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_doc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_lwc.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_object.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_extract_pipeline.py +0 -0
- {graphify_sf-0.3.2 → graphify_sf-0.3.4}/tests/test_llm.py +0 -0
|
@@ -316,26 +316,42 @@ def extract_apex_class(path: Path) -> dict:
|
|
|
316
316
|
)
|
|
317
317
|
)
|
|
318
318
|
|
|
319
|
-
# DML → dml edges
|
|
320
|
-
|
|
319
|
+
# DML → dml edges. The relation stays "dml" (unchanged) but each edge carries an
|
|
320
|
+
# "operation" field derived from the DML verb (insert→create / update→update /
|
|
321
|
+
# delete→delete / upsert / merge / undelete) so downstream can distinguish the
|
|
322
|
+
# kind of write. Native SF verbs (upsert/merge/undelete) are preserved rather than
|
|
323
|
+
# forced into CRUD. confidence stays INFERRED: the DML target is a variable name we
|
|
324
|
+
# cannot statically resolve to an object type — that honesty label must not change.
|
|
325
|
+
# Dedup is by (obj_var, operation) so a class that both inserts and updates the same
|
|
326
|
+
# object yields two edges, not one collapsed dml edge.
|
|
327
|
+
_DML_VERB_TO_OP = {
|
|
328
|
+
"insert": "create",
|
|
329
|
+
"update": "update",
|
|
330
|
+
"delete": "delete",
|
|
331
|
+
"upsert": "upsert",
|
|
332
|
+
"merge": "merge",
|
|
333
|
+
"undelete": "undelete",
|
|
334
|
+
}
|
|
335
|
+
seen_dml_ops: set[tuple[str, str]] = set()
|
|
321
336
|
for dm in _DML_RE.finditer(text):
|
|
337
|
+
operation = _DML_VERB_TO_OP[dm.group(1).lower()]
|
|
322
338
|
obj_var = dm.group(2)
|
|
323
339
|
# obj_var is a variable name, not necessarily an object API name.
|
|
324
340
|
# We record it as INFERRED since we can't always resolve the type statically.
|
|
325
|
-
if obj_var.lower() not in _APEX_KEYWORDS and obj_var not in
|
|
326
|
-
|
|
341
|
+
if obj_var.lower() not in _APEX_KEYWORDS and (obj_var, operation) not in seen_dml_ops:
|
|
342
|
+
seen_dml_ops.add((obj_var, operation))
|
|
327
343
|
# Only add the edge if it looks like a type name (capitalized) or known object
|
|
328
344
|
if obj_var[0].isupper():
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
confidence_score=0.7,
|
|
337
|
-
)
|
|
345
|
+
edge = _make_edge(
|
|
346
|
+
class_nid,
|
|
347
|
+
object_id(obj_var),
|
|
348
|
+
"dml",
|
|
349
|
+
"INFERRED",
|
|
350
|
+
str_path,
|
|
351
|
+
confidence_score=0.7,
|
|
338
352
|
)
|
|
353
|
+
edge["operation"] = operation
|
|
354
|
+
edges.append(edge)
|
|
339
355
|
|
|
340
356
|
# Apex → Flow invocations via Flow.Interview.FlowName
|
|
341
357
|
seen_flow_invokes: set[str] = set()
|
|
@@ -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.4"
|
|
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"
|
|
@@ -385,6 +385,113 @@ public class LeadCreator {
|
|
|
385
385
|
assert e["confidence"] == "INFERRED"
|
|
386
386
|
|
|
387
387
|
|
|
388
|
+
def test_extract_apex_dml_insert_and_update_same_object_two_edges(tmp_path):
|
|
389
|
+
"""A class that both inserts and updates the same object yields TWO dml edges
|
|
390
|
+
(operation=create and operation=update), not one collapsed edge."""
|
|
391
|
+
from graphify_sf.extract.apex import extract_apex_class
|
|
392
|
+
|
|
393
|
+
cls = tmp_path / "AccountWriter.cls"
|
|
394
|
+
cls.write_text(
|
|
395
|
+
"""\
|
|
396
|
+
public class AccountWriter {
|
|
397
|
+
public void work() {
|
|
398
|
+
Account Acct = new Account();
|
|
399
|
+
insert Acct;
|
|
400
|
+
update Acct;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
"""
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
result = extract_apex_class(cls)
|
|
407
|
+
acct_dml = [e for e in result["edges"] if e.get("relation") == "dml" and "acct" in e["target"].lower()]
|
|
408
|
+
assert len(acct_dml) == 2, "insert+update on same object must not be deduped to one edge"
|
|
409
|
+
ops = {e.get("operation") for e in acct_dml}
|
|
410
|
+
assert ops == {"create", "update"}
|
|
411
|
+
for e in acct_dml:
|
|
412
|
+
assert e["confidence"] == "INFERRED"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_extract_apex_dml_pure_update_carries_operation(tmp_path):
|
|
416
|
+
"""A bare DML update produces a dml edge with operation='update' and INFERRED confidence."""
|
|
417
|
+
from graphify_sf.extract.apex import extract_apex_class
|
|
418
|
+
|
|
419
|
+
cls = tmp_path / "AccountUpdater.cls"
|
|
420
|
+
cls.write_text(
|
|
421
|
+
"""\
|
|
422
|
+
public class AccountUpdater {
|
|
423
|
+
public void touch() {
|
|
424
|
+
update Acct;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
"""
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
result = extract_apex_class(cls)
|
|
431
|
+
dml_edges = [e for e in result["edges"] if e.get("relation") == "dml"]
|
|
432
|
+
assert len(dml_edges) == 1
|
|
433
|
+
assert dml_edges[0].get("operation") == "update"
|
|
434
|
+
assert dml_edges[0]["confidence"] == "INFERRED"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def test_extract_apex_dml_native_verbs_preserved(tmp_path):
|
|
438
|
+
"""SF-native DML verbs map to their own operation, not forced into CRUD:
|
|
439
|
+
upsert/undelete are preserved; insert->create, delete->delete, update->update.
|
|
440
|
+
|
|
441
|
+
Note: `merge` uses two-object syntax (`merge a b;`) which the single-object DML
|
|
442
|
+
regex deliberately does not capture — that's an existing extractor boundary, out
|
|
443
|
+
of scope for this change (no object-type resolution upgrade)."""
|
|
444
|
+
from graphify_sf.extract.apex import extract_apex_class
|
|
445
|
+
|
|
446
|
+
cls = tmp_path / "AllDml.cls"
|
|
447
|
+
cls.write_text(
|
|
448
|
+
"""\
|
|
449
|
+
public class AllDml {
|
|
450
|
+
public void run() {
|
|
451
|
+
insert Acct;
|
|
452
|
+
update Con;
|
|
453
|
+
delete Lead;
|
|
454
|
+
upsert Opp;
|
|
455
|
+
undelete Cse;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
"""
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
result = extract_apex_class(cls)
|
|
462
|
+
ops_by_target = {e["target"].lower(): e.get("operation") for e in result["edges"] if e.get("relation") == "dml"}
|
|
463
|
+
assert ops_by_target.get("object_acct") == "create"
|
|
464
|
+
assert ops_by_target.get("object_con") == "update"
|
|
465
|
+
assert ops_by_target.get("object_lead") == "delete"
|
|
466
|
+
assert ops_by_target.get("object_opp") == "upsert"
|
|
467
|
+
assert ops_by_target.get("object_cse") == "undelete"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_extract_apex_soql_queries_edge_unaffected(tmp_path):
|
|
471
|
+
"""Regression: SOQL still produces a 'queries' edge (EXTRACTED) with no 'operation'
|
|
472
|
+
field — only DML edges gained operation."""
|
|
473
|
+
from graphify_sf.extract.apex import extract_apex_class
|
|
474
|
+
|
|
475
|
+
cls = tmp_path / "AccountReader.cls"
|
|
476
|
+
cls.write_text(
|
|
477
|
+
"""\
|
|
478
|
+
public class AccountReader {
|
|
479
|
+
public void read() {
|
|
480
|
+
List<Account> a = [SELECT Id FROM Account];
|
|
481
|
+
update Acct;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
"""
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
result = extract_apex_class(cls)
|
|
488
|
+
query_edges = [e for e in result["edges"] if e.get("relation") == "queries"]
|
|
489
|
+
assert len(query_edges) >= 1
|
|
490
|
+
for e in query_edges:
|
|
491
|
+
assert e["confidence"] == "EXTRACTED"
|
|
492
|
+
assert e.get("operation") is None, "queries edges must not carry an operation field"
|
|
493
|
+
|
|
494
|
+
|
|
388
495
|
# ---------------------------------------------------------------------------
|
|
389
496
|
# Apex trigger fallback (no regex match → filename)
|
|
390
497
|
# ---------------------------------------------------------------------------
|
|
@@ -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
|