Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load data source configs from constants #354

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from
69 changes: 69 additions & 0 deletions inc/Analytics/DataSourceAnalytics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace RemoteDataBlocks\Analytics;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please name this class and folder using Telemetry instead of analytics?


use RemoteDataBlocks\Analytics\TracksAnalytics;
use RemoteDataBlocks\Config\DataSource\DataSourceConfigManager;

defined( 'ABSPATH' ) || exit();

class DataSourceAnalytics {
const DATA_SOURCE_INTERACTION_EVENT_NAME = 'remotedatablocks_data_source_interaction';
const DATA_SOURCE_VIEW_EVENT_NAME = 'remotedatablocks_view_data_sources';
const DATA_SOURCE_VIEW_TRACK_TRANSIENT_KEY = 'remotedatablocks_view_data_sources_tracked';
Comment on lines +11 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be specifying a prefix in the Tracks constructor instead of repeating it each time


private static function get_interaction_track_props( array $config ): array {
$props = [];

if ( 'generic-http' === $config['service'] ) {
$auth = $config['service_config']['auth'] ?? [];
$props['authentication_type'] = $auth['type'] ?? '';
$props['api_key_location'] = $auth['addTo'] ?? '';
}

return $props;
}

private static function track_interaction( array $config, string $action ): void {
TracksAnalytics::record_event( self::DATA_SOURCE_INTERACTION_EVENT_NAME, array_merge( [
'data_source_type' => $config['service'],
'action' => $action,
], self::get_interaction_track_props( $config ) ) );
}

public static function track_add( array $config ): void {
self::track_interaction( $config, 'add' );
}

public static function track_update( array $config ): void {
self::track_interaction( $config, 'update' );
}

public static function track_delete( array $config ): void {
self::track_interaction( $config, 'delete' );
}

public static function track_view( array $configs ): void {
/**
* Tracks Analytics. Only once per day to reduce noise.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it noisy and why wouldn't we want an accurate count? Can we call this when rendering the screen instead of from the REST endpoint?

*/
if ( ! get_transient( self::DATA_SOURCE_VIEW_TRACK_TRANSIENT_KEY ) ) {
$code_configured_count = count( array_filter(
$configs,
fn ( $config ) => DataSourceConfigManager::CONFIG_SOURCE_CODE === $config['config_source']
) );
$storage_configured_count = count( array_filter(
$configs,
fn ( $config ) => DataSourceConfigManager::CONFIG_SOURCE_STORAGE === $config['config_source']
) );

TracksAnalytics::record_event( self::DATA_SOURCE_VIEW_EVENT_NAME, [
'total_data_sources_count' => count( $configs ),
'code_configured_data_sources_count' => $code_configured_count,
'ui_configured_data_sources_count' => $storage_configured_count,
] );

set_transient( self::DATA_SOURCE_VIEW_TRACK_TRANSIENT_KEY, true, DAY_IN_SECONDS );
}
}
}
95 changes: 95 additions & 0 deletions inc/Config/DataSource/ConstantConfigStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types = 1);

namespace RemoteDataBlocks\Config\DataSource;

use RemoteDataBlocks\Config\DataSource\DataSourceInterface;
use RemoteDataBlocks\Logging\LoggerManager;
use WP_Error;

use const RemoteDataBlocks\REMOTE_DATA_BLOCKS__DATA_SOURCE_CLASSMAP;

class ConstantConfigStore {
Copy link
Member

@chriszarate chriszarate Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important for inc/Config to stayed focused on interfaces and query primitives for users to explore when extending. Can we move this to include/Integrations or another location?

private const CONFIG_CONSTANT_NAME = 'REMOTE_DATA_BLOCKS_CONFIGS';

/**
* Get all configurations from the constant.
*
* @return array<array{
* service: string,
* service_config: array<string, mixed>
* }>
*/
public static function get_configs(): array {
if ( ! self::is_available() ) {
return [];
}

$configs = constant( self::CONFIG_CONSTANT_NAME );
$valid_configs = [];

foreach ( $configs as $config ) {
$validation_result = self::validate_config( $config );
if ( ! is_wp_error( $validation_result ) ) {
$valid_configs[] = $config;
} else {
LoggerManager::instance()->error(
sprintf(
'Invalid data source config found (uuid: %s): %s',
$config['uuid'] ?? 'unknown',
$validation_result->get_error_message()
)
);
}
}

return $valid_configs;
}

public static function get_config_by_uuid( string $uuid ): array|WP_Error {
$configs = constant( self::CONFIG_CONSTANT_NAME );
$found = array_filter( $configs, fn ( $config ) => $config['uuid'] === $uuid );

if ( empty( $found ) ) {
return new WP_Error(
'data_source_not_found',
__( 'Data source not found', 'remote-data-blocks' ),
[ 'status' => 404 ]
);
}

$config = reset( $found );
$validation_result = self::validate_config( $config );

if ( is_wp_error( $validation_result ) ) {
return $validation_result;
}

return $config;
}

public static function validate_config( array $config ): DataSourceInterface|WP_Error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update methods to private where not needed externally? Tests should target true public methods

$data_source_class = REMOTE_DATA_BLOCKS__DATA_SOURCE_CLASSMAP[ $config['service'] ] ?? null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using the classmap, we should use the new __class property from #370

if ( null === $data_source_class ) {
return new WP_Error(
'unsupported_data_source',
__( 'Unsupported data source service', 'remote-data-blocks' )
);
}

return $data_source_class::from_array( $config );
}

/**
* Check if constant configuration is available and valid.
*
* @return bool Whether the constant configuration is available and valid.
*/
public static function is_available(): bool {
if ( ! defined( self::CONFIG_CONSTANT_NAME ) || '' === constant( self::CONFIG_CONSTANT_NAME ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

covered by is_array below

Suggested change
if ( ! defined( self::CONFIG_CONSTANT_NAME ) || '' === constant( self::CONFIG_CONSTANT_NAME ) ) {
if ( ! defined( self::CONFIG_CONSTANT_NAME ) ) {

return false;
}

$value = constant( self::CONFIG_CONSTANT_NAME );
return is_array( $value );
}
}
233 changes: 233 additions & 0 deletions inc/Config/DataSource/DataSourceConfigManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?php declare(strict_types = 1);

namespace RemoteDataBlocks\Config\DataSource;

use RemoteDataBlocks\Editor\BlockManagement\ConfigStore;
use RemoteDataBlocks\WpdbStorage\DataSourceCrud;
use RemoteDataBlocks\Config\DataSource\ConstantConfigStore;
use WP_Error;

class DataSourceConfigManager {
public const CONFIG_SOURCE_CODE = 'code';
public const CONFIG_SOURCE_STORAGE = 'storage';
public const CONFIG_SOURCE_CONSTANT = 'constant';
public const MUTABLE_CONFIG_SOURCES = [ self::CONFIG_SOURCE_STORAGE ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice


private static function get_all_from_storage(): array {
return array_map(
fn ( array $config ) => array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should be consistent and use either all arrow functions or all anonymous function closures? The behavior of scope is different between the two

DataSourceCrud::get_configs()
);
}

private static function get_all_from_constant(): array {
return array_map(
fn ( array $config ) => array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_CONSTANT ] ),
ConstantConfigStore::get_configs()
);
}

private static function get_all_from_code(): array {
return array_map(
fn ( array $config ) => array_merge( $config, [ 'config_source' => self::CONFIG_SOURCE_CODE ] ),
ConfigStore::get_data_sources_as_array()
);
}

/**
* Quick and dirty de-duplication of data sources. If the data source does
* not have a UUID (because it is registered in code), we generate an
* identifier based on the display name and service name.
*/
private static function de_duplicate_configs( array $configs ): array {
return array_values( array_reduce(
$configs,
function ( array $acc, array $item ) {
$identifier = $item['uuid'] ?? md5(
sprintf( '%s_%s', $item['service_config']['display_name'], $item['service'] )
);
$acc[ $identifier ] = $item;
return $acc;
},
[]
) );
}

/**
* Get all data sources from all origins.
*
* @return array<array{
* uuid?: string,
* service: string,
* service_config: array<string, mixed>,
* config_source: string,
* __metadata?: array{
* created_at: string,
* updated_at: string
* }
* }>
*/
public static function get_all(): array {
$code_configured = self::get_all_from_code();
$constant_configured = self::get_all_from_constant();
$storage_configured = self::get_all_from_storage();

/**
* De-duplicate configs.
*
* Precedence (lowest to highest):
* - Code-configured data sources
* - Constant-configured data sources
* - Storage-configured data sources
*/
return self::de_duplicate_configs(
array_merge( $code_configured, $constant_configured, $storage_configured )
);
}

/**
* Get a data source by its UUID.
*
* @param string $uuid The UUID of the data source to get.
* @return array{
* uuid: string,
* service: string,
* service_config: array<string, mixed>,
* config_source: string,
* __metadata?: array{
* created_at: string,
* updated_at: string
* }
* }|WP_Error
*/
public static function get( string $uuid ): array|WP_Error {
$from_constant = ConstantConfigStore::get_config_by_uuid( $uuid );
if ( ! is_wp_error( $from_constant ) ) {
return array_merge(
$from_constant,
[ 'config_source' => self::CONFIG_SOURCE_CONSTANT ]
);
}

$from_storage = DataSourceCrud::get_config_by_uuid( $uuid );
if ( ! is_wp_error( $from_storage ) ) {
return array_merge(
$from_storage,
[ 'config_source' => self::CONFIG_SOURCE_STORAGE ]
);
}

return new WP_Error(
'data_source_not_found',
__( 'Data source not found', 'remote-data-blocks' ),
[ 'status' => 404 ]
);
}

/**
* Create a new data source.
*
* @param array $config The configuration for the new data source.
* @return array{
* uuid: string,
* service: string,
* service_config: array<string, mixed>,
* config_source: string,
* __metadata: array{
* created_at: string,
* updated_at: string
* }
* }|WP_Error
*/
public static function create( array $config ): array|WP_Error {
$result = DataSourceCrud::create_config( $config );
if ( is_wp_error( $result ) ) {
return $result;
}

return array_merge( $result, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] );
}

/**
* Update a data source.
*
* @param string $uuid The UUID of the data source to update.
* @param array $config The new configuration for the data source.
* @return array{
* uuid: string,
* service: string,
* service_config: array<string, mixed>,
* config_source: string,
* __metadata: array{
* created_at: string,
* updated_at: string
* }
* }|WP_Error
*/
public static function update( string $uuid, array $config ): array|WP_Error {
if (
isset( $config['config_source'] ) &&
! in_array( $config['config_source'], self::MUTABLE_CONFIG_SOURCES, true )
) {
/**
* Only storage-configured data sources are mutable.
*/
return new WP_Error(
'cannot_update_config',
__( 'Cannot update a data source with this config_source', 'remote-data-blocks' ),
[ 'status' => 400 ]
);
}

$result = DataSourceCrud::update_config_by_uuid( $uuid, $config );
if ( is_wp_error( $result ) ) {
return $result;
}

return array_merge( $result, [ 'config_source' => self::CONFIG_SOURCE_STORAGE ] );
}

/**
* Delete a data source.
*
* @param string $uuid The UUID of the data source to delete.
* @return true|WP_Error True on success, WP_Error on failure.
*/
public static function delete( string $uuid ): bool|WP_Error {
return DataSourceCrud::delete_config_by_uuid( $uuid );
}

/**
* Get all configured data sources for a specific service from both storage and constants.
* This includes sources configured via storage and constants, but not those defined in code.
Comment on lines +201 to +202
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently excluding these configs from a method named get_all_... feels wrong. Shouldn't the configs declare how they want to be treated? E.g., if they want queries and blocks registered for them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a code-configured data source wants to have queries registered for it?

*
* @param string $service The service identifier to filter by.
* @return array<array{
* uuid?: string,
* service: string,
* service_config: array<string, mixed>,
* config_source: string,
* __metadata?: array{
* created_at: string,
* updated_at: string
* }
* }>
*/
public static function get_all_configured_by_service( string $service ): array {
$storage_configs = self::get_all_from_storage();
$constant_configs = self::get_all_from_constant();

$all_configs = array_merge( $constant_configs, $storage_configs );

/**
* De-duplicate configs.
*
* Precedence (lowest to highest):
* - Constant-configured data sources
* - Storage-configured data sources
*/
$de_duplicated_configs = self::de_duplicate_configs( $all_configs );

return array_filter( $de_duplicated_configs, fn ( array $config ) => $config['service'] === $service );
}
}
Loading
Loading