Skip to content

Commit 00d7b92

Browse files
committed
feat: add flow config pre-save filter
1 parent b5bdd8a commit 00d7b92

2 files changed

Lines changed: 199 additions & 2 deletions

File tree

inc/Core/Database/Flows/Flows.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ public function create_flow( array $flow_data ) {
183183
}
184184
}
185185

186-
$flow_config = wp_json_encode( $flow_data['flow_config'] );
186+
$flow_config = $this->prepare_flow_config_for_save( $flow_data['flow_config'], 0, $flow_data );
187+
if ( false === $flow_config ) {
188+
return false;
189+
}
190+
191+
$flow_config = wp_json_encode( $flow_config );
187192
$scheduling_config = wp_json_encode( $flow_data['scheduling_config'] );
188193

189194
$user_id = isset( $flow_data['user_id'] ) ? absint( $flow_data['user_id'] ) : 0;
@@ -791,7 +796,12 @@ public function update_flow( int $flow_id, array $flow_data ): bool {
791796
}
792797

793798
if ( isset( $flow_data['flow_config'] ) ) {
794-
$update_data['flow_config'] = wp_json_encode( $flow_data['flow_config'] );
799+
$flow_config = $this->prepare_flow_config_for_save( $flow_data['flow_config'], $flow_id, $flow_data );
800+
if ( false === $flow_config ) {
801+
return false;
802+
}
803+
804+
$update_data['flow_config'] = wp_json_encode( $flow_config );
795805
$update_formats[] = '%s';
796806
}
797807

@@ -856,6 +866,32 @@ public function update_flow( int $flow_id, array $flow_data ): bool {
856866
return true;
857867
}
858868

869+
/**
870+
* Apply the flow-config write-boundary filter before persisting JSON.
871+
*
872+
* @param array<string,mixed> $flow_config Flow config payload.
873+
* @param int $flow_id Flow ID, or 0 while creating a new flow.
874+
* @param array<string,mixed> $flow_data Full create/update payload.
875+
* @return array<string,mixed>|false Filtered flow config, or false when rejected.
876+
*/
877+
private function prepare_flow_config_for_save( array $flow_config, int $flow_id, array $flow_data ) {
878+
$filtered_config = apply_filters( 'datamachine_flow_config_pre_save', $flow_config, $flow_id, $flow_data );
879+
if ( is_wp_error( $filtered_config ) ) {
880+
do_action(
881+
'datamachine_log',
882+
'error',
883+
'flow_config rejected by datamachine_flow_config_pre_save filter',
884+
array(
885+
'flow_id' => $flow_id,
886+
'reason' => $filtered_config->get_error_message(),
887+
)
888+
);
889+
return false;
890+
}
891+
892+
return is_array( $filtered_config ) ? $filtered_config : $flow_config;
893+
}
894+
859895
/**
860896
* Delete a flow
861897
*/
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
/**
3+
* Pure-PHP smoke test for the flow_config pre-save filter.
4+
*
5+
* Run with: php tests/flow-config-pre-save-filter-smoke.php
6+
*
7+
* @package DataMachine\Tests
8+
*/
9+
10+
define( 'ABSPATH', __DIR__ . '/' );
11+
define( 'ARRAY_A', 'ARRAY_A' );
12+
13+
$GLOBALS['datamachine_flow_config_pre_save_filters'] = array();
14+
$GLOBALS['datamachine_flow_config_pre_save_logs'] = array();
15+
16+
class WP_Error {
17+
private string $message;
18+
19+
public function __construct( string $code, string $message ) {
20+
unset( $code );
21+
$this->message = $message;
22+
}
23+
24+
public function get_error_message(): string {
25+
return $this->message;
26+
}
27+
}
28+
29+
function add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): void {
30+
unset( $priority, $accepted_args );
31+
$GLOBALS['datamachine_flow_config_pre_save_filters'][ $hook_name ][] = $callback;
32+
}
33+
34+
function apply_filters( string $hook_name, $value, ...$args ) {
35+
foreach ( $GLOBALS['datamachine_flow_config_pre_save_filters'][ $hook_name ] ?? array() as $callback ) {
36+
$value = $callback( $value, ...$args );
37+
}
38+
39+
return $value;
40+
}
41+
42+
function do_action( string $hook_name, ...$args ): void {
43+
$GLOBALS['datamachine_flow_config_pre_save_logs'][] = array( $hook_name, $args );
44+
}
45+
46+
function is_wp_error( $value ): bool {
47+
return $value instanceof WP_Error;
48+
}
49+
50+
function wp_json_encode( $value ): string {
51+
return json_encode( $value );
52+
}
53+
54+
function sanitize_text_field( $value ): string {
55+
return trim( (string) $value );
56+
}
57+
58+
function sanitize_title( $value ): string {
59+
return strtolower( preg_replace( '/[^a-z0-9-]+/i', '-', trim( (string) $value ) ) );
60+
}
61+
62+
function absint( $value ): int {
63+
return max( 0, (int) $value );
64+
}
65+
66+
final class DataMachineFlowConfigPreSaveFakeWpdb {
67+
public string $prefix = 'wp_';
68+
public int $insert_id = 123;
69+
public string $last_error = '';
70+
public array $inserted = array();
71+
public array $updated = array();
72+
73+
public function insert( string $table, array $data, array $formats ) {
74+
$this->inserted[] = compact( 'table', 'data', 'formats' );
75+
return 1;
76+
}
77+
78+
public function update( string $table, array $data, array $where, array $formats, array $where_formats ) {
79+
$this->updated[] = compact( 'table', 'data', 'where', 'formats', 'where_formats' );
80+
return 1;
81+
}
82+
}
83+
84+
$GLOBALS['wpdb'] = new DataMachineFlowConfigPreSaveFakeWpdb();
85+
86+
require_once dirname( __DIR__ ) . '/inc/Core/Database/BaseRepository.php';
87+
require_once dirname( __DIR__ ) . '/inc/Core/Database/Flows/Flows.php';
88+
89+
use DataMachine\Core\Database\Flows\Flows;
90+
91+
function datamachine_flow_config_pre_save_assert( bool $condition, string $message ): void {
92+
if ( ! $condition ) {
93+
fwrite( STDERR, "FAIL {$message}\n" );
94+
exit( 1 );
95+
}
96+
97+
echo "PASS {$message}\n";
98+
}
99+
100+
echo "flow-config-pre-save-filter-smoke\n";
101+
102+
$flows = new Flows();
103+
104+
add_filter(
105+
'datamachine_flow_config_pre_save',
106+
static function ( array $flow_config, int $flow_id, array $flow_data ) {
107+
$flow_config['step_one']['handler_configs']['upsert_event']['taxonomy_location_selection'] = 'derived';
108+
$flow_config['step_one']['seen_flow_id'] = $flow_id;
109+
$flow_config['step_one']['seen_flow_name'] = $flow_data['flow_name'] ?? '';
110+
return $flow_config;
111+
},
112+
10,
113+
3
114+
);
115+
116+
$created = $flows->create_flow(
117+
array(
118+
'pipeline_id' => 10,
119+
'flow_name' => ' Event Flow ',
120+
'flow_config' => array(
121+
'step_one' => array(
122+
'handler_configs' => array(
123+
'upsert_event' => array( 'taxonomy_location_selection' => 'ai_decides' ),
124+
),
125+
),
126+
),
127+
'scheduling_config' => array(),
128+
)
129+
);
130+
131+
datamachine_flow_config_pre_save_assert( 123 === $created, 'create_flow returns the inserted flow ID' );
132+
$created_config = json_decode( $GLOBALS['wpdb']->inserted[0]['data']['flow_config'], true );
133+
datamachine_flow_config_pre_save_assert( 'derived' === $created_config['step_one']['handler_configs']['upsert_event']['taxonomy_location_selection'], 'create_flow persists filtered flow_config' );
134+
datamachine_flow_config_pre_save_assert( 0 === $created_config['step_one']['seen_flow_id'], 'create_flow passes flow_id 0 to the pre-save filter' );
135+
136+
$updated = $flows->update_flow(
137+
456,
138+
array(
139+
'flow_name' => 'Updated Flow',
140+
'flow_config' => array( 'step_one' => array() ),
141+
)
142+
);
143+
144+
datamachine_flow_config_pre_save_assert( true === $updated, 'update_flow succeeds when the filter returns an array' );
145+
$updated_config = json_decode( $GLOBALS['wpdb']->updated[0]['data']['flow_config'], true );
146+
datamachine_flow_config_pre_save_assert( 456 === $updated_config['step_one']['seen_flow_id'], 'update_flow passes the persisted flow ID to the pre-save filter' );
147+
datamachine_flow_config_pre_save_assert( 'Updated Flow' === $updated_config['step_one']['seen_flow_name'], 'update_flow passes the full update payload to the pre-save filter' );
148+
149+
$GLOBALS['datamachine_flow_config_pre_save_filters']['datamachine_flow_config_pre_save'] = array(
150+
static fn() => new WP_Error( 'invalid_flow_config', 'location must be derived' ),
151+
);
152+
153+
$before_update_count = count( $GLOBALS['wpdb']->updated );
154+
$rejected = $flows->update_flow( 789, array( 'flow_config' => array( 'step_one' => array() ) ) );
155+
156+
datamachine_flow_config_pre_save_assert( false === $rejected, 'update_flow rejects WP_Error filter results' );
157+
datamachine_flow_config_pre_save_assert( $before_update_count === count( $GLOBALS['wpdb']->updated ), 'rejected flow_config is not written' );
158+
$last_log = end( $GLOBALS['datamachine_flow_config_pre_save_logs'] );
159+
datamachine_flow_config_pre_save_assert( 'location must be derived' === ( $last_log[1][2]['reason'] ?? '' ), 'rejection reason is logged' );
160+
161+
echo "All flow config pre-save filter assertions passed.\n";

0 commit comments

Comments
 (0)