Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions data-machine.php
Original file line number Diff line number Diff line change
Expand Up @@ -460,13 +460,32 @@ function datamachine_deactivate_plugin() {
* @param bool $network_wide Whether the plugin is being network-activated.
*/
function datamachine_activate_plugin( $network_wide = false ) {
// Agent tables are network-scoped — create once regardless of activation mode.
datamachine_create_network_agent_tables();

if ( is_multisite() && $network_wide ) {
datamachine_for_each_site( 'datamachine_activate_for_site' );
} else {
datamachine_activate_for_site();
}
}

/**
* Create network-scoped agent tables.
*
* Agent identity, tokens, and access grants are shared across the multisite
* network, following the WordPress pattern where wp_users/wp_usermeta use
* base_prefix while per-site content uses site-specific prefixes.
*
* Safe to call multiple times — dbDelta is idempotent.
*/
function datamachine_create_network_agent_tables() {
\DataMachine\Core\Database\Agents\Agents::create_table();
\DataMachine\Core\Database\Agents\Agents::ensure_site_scope_column();
\DataMachine\Core\Database\Agents\AgentAccess::create_table();
\DataMachine\Core\Database\Agents\AgentTokens::create_table();
}

/**
* Run activation tasks for a single site.
*
Expand All @@ -480,10 +499,9 @@ function datamachine_activate_for_site() {
// Create logs table first — other table migrations log messages during creation.
\DataMachine\Core\Database\Logs\LogRepository::create_table();

// Ensure first-class agents table exists.
\DataMachine\Core\Database\Agents\Agents::create_table();
\DataMachine\Core\Database\Agents\AgentAccess::create_table();
\DataMachine\Core\Database\Agents\AgentTokens::create_table();
// Agent tables are network-scoped (base_prefix) — ensure they exist.
// Safe to call per-site because dbDelta + base_prefix is idempotent.
datamachine_create_network_agent_tables();

$db_pipelines = new \DataMachine\Core\Database\Pipelines\Pipelines();
$db_pipelines->create_table();
Expand Down Expand Up @@ -524,6 +542,9 @@ function datamachine_activate_for_site() {
// Migrate USER.md to network-scoped paths and create NETWORK.md on multisite (idempotent).
datamachine_migrate_user_md_to_network_scope();

// Migrate per-site agents to network-scoped tables (idempotent).
datamachine_migrate_agents_to_network_scope();

// Regenerate SITE.md with enriched content and clean up legacy SiteContext transient.
datamachine_regenerate_site_md();
delete_transient( 'datamachine_site_context_data' );
Expand Down
4 changes: 2 additions & 2 deletions inc/Abilities/AgentAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,12 @@ public static function deleteAgent( array $input ): array {

// Delete access grants.
global $wpdb;
$access_table = $wpdb->prefix . 'datamachine_agent_access';
$access_table = $wpdb->base_prefix . 'datamachine_agent_access';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete( $access_table, array( 'agent_id' => $agent_id ) );

// Delete agent record.
$agents_table = $wpdb->prefix . 'datamachine_agents';
$agents_table = $wpdb->base_prefix . 'datamachine_agents';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$deleted = $wpdb->delete( $agents_table, array( 'agent_id' => $agent_id ) );

Expand Down
12 changes: 11 additions & 1 deletion inc/Core/Database/Agents/AgentAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ class AgentAccess extends BaseRepository {
*/
const VALID_ROLES = array( 'admin', 'operator', 'viewer' );

/**
* Use network-level prefix so access grants are shared across the multisite network.
*
* @return string
*/
protected static function get_table_prefix(): string {
global $wpdb;
return $wpdb->base_prefix;
}

/**
* Create agent_access table.
*
Expand All @@ -41,7 +51,7 @@ class AgentAccess extends BaseRepository {
public static function create_table(): void {
global $wpdb;

$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = $wpdb->base_prefix . self::TABLE_NAME;
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$table_name} (
Expand Down
12 changes: 11 additions & 1 deletion inc/Core/Database/Agents/AgentTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ class AgentTokens extends BaseRepository {
*/
const TOKEN_PREFIX = 'datamachine_';

/**
* Use network-level prefix so tokens are shared across the multisite network.
*
* @return string
*/
protected static function get_table_prefix(): string {
global $wpdb;
return $wpdb->base_prefix;
}

/**
* Create agent_tokens table.
*
Expand All @@ -41,7 +51,7 @@ class AgentTokens extends BaseRepository {
public static function create_table(): void {
global $wpdb;

$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = $wpdb->base_prefix . self::TABLE_NAME;
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$table_name} (
Expand Down
37 changes: 35 additions & 2 deletions inc/Core/Database/Agents/Agents.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ class Agents extends BaseRepository {
*/
const TABLE_NAME = 'datamachine_agents';

/**
* Use network-level prefix so agents are shared across the multisite network.
*
* @return string
*/
protected static function get_table_prefix(): string {
global $wpdb;
return $wpdb->base_prefix;
}

/**
* Create agents table.
*
Expand All @@ -31,28 +41,51 @@ class Agents extends BaseRepository {
public static function create_table(): void {
global $wpdb;

$table_name = $wpdb->prefix . self::TABLE_NAME;
$table_name = $wpdb->base_prefix . self::TABLE_NAME;
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$table_name} (
agent_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
agent_slug VARCHAR(200) NOT NULL,
agent_name VARCHAR(200) NOT NULL,
owner_id BIGINT(20) UNSIGNED NOT NULL,
site_scope BIGINT(20) UNSIGNED NULL DEFAULT NULL,
agent_config LONGTEXT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (agent_id),
UNIQUE KEY agent_slug (agent_slug),
KEY owner_id (owner_id),
KEY status (status)
KEY status (status),
KEY site_scope (site_scope)
) {$charset_collate};";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}

/**
* Ensure site_scope column exists on existing installs.
*
* @return void
*/
public static function ensure_site_scope_column(): void {
global $wpdb;

$table_name = $wpdb->base_prefix . self::TABLE_NAME;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$column = $wpdb->get_var( "SHOW COLUMNS FROM `{$table_name}` LIKE 'site_scope'" );

if ( ! $column ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "ALTER TABLE `{$table_name}` ADD COLUMN site_scope BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER owner_id" );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query( "ALTER TABLE `{$table_name}` ADD KEY site_scope (site_scope)" );
}
}

/**
* Get agent by agent ID.
*
Expand Down
17 changes: 16 additions & 1 deletion inc/Core/Database/BaseRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,22 @@ abstract class BaseRepository {
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->table_name = $wpdb->prefix . static::TABLE_NAME;
$this->table_name = static::get_table_prefix() . static::TABLE_NAME;
}

/**
* Get the table prefix for this repository.
*
* Defaults to $wpdb->prefix (per-site). Network-scoped repositories
* (agents, tokens, access) override this to return $wpdb->base_prefix
* so their tables are shared across the multisite network, following
* the same pattern WordPress uses for wp_users and wp_usermeta.
*
* @return string Table prefix.
*/
protected static function get_table_prefix(): string {
global $wpdb;
return $wpdb->prefix;
}

/**
Expand Down
Loading
Loading