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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
155 changes: 102 additions & 53 deletions ProcessMaker/Http/Controllers/Api/V1_1/ProcessVariableController.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,16 +5,20 @@
namespace ProcessMaker\Http\Controllers\Api\V1_1;

use ProcessMaker\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;

class ProcessVariableController extends Controller
{

const CACHE_TTL = 60;
private static bool $mockData = false;

/**
* @OA\Schema(
* schema="Variable",
Expand DownExpand Up@@ -113,24 +117,12 @@ public function index(Request $request)
$excludeSavedSearch = $validated['savedSearchId'] ?? 0;

// Generate mock data
$mockData = $this->generateMockData($processIds);
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
if (static::$mockData) {
$paginator = $this->getProcessesVariablesFromMock($processIds, $excludeSavedSearch, $page, $perPage, $request);
} else {
$paginator = $this->getProcessesVariables($processIds, $excludeSavedSearch, $page, $perPage, $request);
}

// Create paginator
$paginator = new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);

return response()->json([
'data' => array_values($paginator->items()),
'meta' => [
Expand All@@ -151,51 +143,108 @@ public function index(Request $request)
]);
}

private function generateMockData(array $processIds): Collection
/**
* Retrieve process variables from a mock source.
*
* @param array $processIds An array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to determine whether to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
*
* @return array The list of process variables.
*/
private function getProcessesVariablesFromMock(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(5), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'uuid' => (string) Str::uuid(),
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'asset' => [
'id' => "asset_{$processId}_{$i}",
'type' => $this->getRandomAssetType(),
'name' => "Asset {$i} for Process {$processId}",
'uuid' => (string) Str::uuid(),
],
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
$mockData = Cache::remember($cacheKey, now()->addSeconds(self::CACHE_TTL), function () use ($excludeSavedSearch) {
if (!$excludeSavedSearch) {
return collect();
}

return $variables;
$savedSearch = SavedSearch::find($excludeSavedSearch);
return collect(array_values($savedSearch->data_columns->toArray()));
});

return $variables;
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
$columns = $savedSearch->current_columns;
$mockData = $mockData->filter(function ($variable) use ($columns) {
return !$columns->pluck('field')->contains($variable['field']);
});
}

// Create paginator
return new LengthAwarePaginator(
$mockData->forPage($page, $perPage),
$mockData->count(),
$perPage,
$page,
['path' => $request->url()]
);
}

private function getRandomDataType(): string
/**
* Retrieve process variables for the given process IDs.
*
* @param array $processIds Array of process IDs to retrieve variables for.
* @param bool $excludeSavedSearch Flag to exclude saved searches.
* @param int $page The page number for pagination.
* @param int $perPage The number of items per page for pagination.
* @param \Illuminate\Http\Request $request The HTTP request instance.
* @return \Illuminate\Http\JsonResponse JSON response containing the process variables.
*/
public function getProcessesVariables(array $processIds, $excludeSavedSearch, $page, $perPage, $request)
{
return collect(['string', 'number', 'boolean', 'array'])->random();
// If the classes or tables do not exist, fallback to a saved search approach.
if (!class_exists(ProcessVariable::class) || !Schema::hasTable('process_variables')) {
return $this->getProcessesVariablesFromSavedSearch($processIds);
}

// Determine which columns to exclude based on the saved search
$activeColumns = [];
if ($excludeSavedSearch) {
$savedSearch = SavedSearch::find($excludeSavedSearch);
if ($savedSearch && $savedSearch->current_columns) {
$activeColumns = $savedSearch->current_columns->pluck('field')->toArray();
}
}

// Build a single query that joins process_variables, asset_variables, and var_finder_variables
// and applies filtering for excluded fields.
$query = DB::table('var_finder_variables AS vfv')
->join('asset_variables AS av', 'vfv.asset_variable_id', '=', 'av.id')
->join('process_variables AS pv', 'av.id', '=', 'pv.asset_variable_id')
->whereIn('pv.process_id', $processIds);

if (!empty($activeColumns)) {
$query->whereNotIn('vfv.field', $activeColumns);
}

// Paginate the query result directly
return $query->select(
'vfv.id',
'pv.process_id',
'vfv.data_type AS format',
'vfv.label',
'vfv.field',
DB::raw('NULL AS `default`'),
'vfv.created_at',
'vfv.updated_at',
)->paginate($perPage, ['*'], 'page', $page);
}

private function getRandomAssetType(): string
/**
* Change ProcessVariableController to use mock data
*
* @return void
*/
public static function mock()
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
static::$mockData = true;
}
}
}
123 changes: 116 additions & 7 deletions tests/Feature/Api/V1_1/ProcessVariableControllerTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,15 +2,26 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use ProcessMaker\Models\User;
use ProcessMaker\Package\SavedSearch\Models\SavedSearch;
use Tests\TestCase;
use Tests\Feature\Shared\RequestHelper;
use ProcessMaker\Package\VariableFinder\Models\ProcessVariable;
use Illuminate\Support\Str;
use ProcessMaker\Http\Controllers\Api\V1_1\ProcessVariableController;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\Screen;
use ProcessMaker\Package\VariableFinder\Models\AssetVariable;
use ProcessMaker\Package\VariableFinder\Models\VarFinderVariable;

class ProcessVariableControllerTest extends TestCase
{
use RequestHelper;

private bool $isVariablesFinderEnabled;

/**
* Set up test environment by creating a test user and authenticating as them
*
Expand All@@ -20,6 +31,19 @@ public function setupCreateUser()
{
$this->user = User::factory()->create();
$this->actingAs($this->user);

// Check if the VariableFinder package is enabled
$this->isVariablesFinderEnabled = class_exists(ProcessVariable::class) && Schema::hasTable('process_variables');

// Create the processes variables
if (!$this->isVariablesFinderEnabled) {
// Mock the ProcessVariableController to use mock data instead of VariableFinder package
ProcessVariableController::mock();
$this->mockVariableFinder([1, 2, 3], null);
$this->mockVariableFinder([1, 2], null);
} else {
$this->loadVariableFinderData([1, 2, 3]);
}
}

/**
Expand All@@ -37,16 +61,10 @@ public function test_can_get_process_variables_with_pagination(): void
'*' => [
'id',
'process_id',
'uuid',
'format',
'label',
'field',
'asset' => [
'id',
'type',
'name',
'uuid',
],
'default',
'created_at',
'updated_at',
]
Expand All@@ -71,6 +89,94 @@ public function test_can_get_process_variables_with_pagination(): void
$this->assertEquals(30, $responseData['meta']['total']);
}

private function mockVariableFinder(array $processIds, $excludeSavedSearch)
{
// Create a cache key based on process IDs
$cacheKey = 'process_variables_' . implode('_', $processIds);
if ($excludeSavedSearch) {
$cacheKey .= '_exclude_saved_search_' . $excludeSavedSearch;
}

// Try to get variables from cache first
$variables = Cache::remember($cacheKey, now()->addSeconds(60), function () use ($processIds) {
$variables = collect();

foreach ($processIds as $processId) {
// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {
$variables->push([
'id' => $variables->count() + 1,
'process_id' => $processId,
'format' => $this->getRandomDataType(),
'label' => "Variable {$i} for Process {$processId}",
'field' => "data.var_{$processId}_{$i}",
'default' => null,
'created_at' => now()->toIso8601String(),
'updated_at' => now()->toIso8601String(),
]);
}
}

return $variables;
});

return $variables;
}

private function getRandomDataType(): string
{
return collect(['string', 'int', 'boolean', 'array'])->random();
}

private function getRandomAssetType(): string
{
return collect(['sensor', 'actuator', 'controller', 'device'])->random();
}

private function loadVariableFinderData(array $processIds)
{
foreach ($processIds as $processId) {
$process = Process::factory()->create([
'id' => $processId,
]);
// 1. Create the AssetVariable record
$asset = [
'type' => $this->getRandomAssetType(),
'uuid' => (string) Str::uuid(),
];
$assetVariable = AssetVariable::create([
'uuid' => $asset['uuid'],
'asset_id' => 1, // Scren id=1
'asset_type' => Screen::class,
]);

// 2. Create the ProcessVariable record linking to the AssetVariable
ProcessVariable::create([
'uuid' => (string) Str::uuid(),
'process_id' => $processId,
'asset_variable_id' => $assetVariable->id,
]);

// Generate 10 variables per process
for ($i = 1; $i <= 10; $i++) {

// Generate data similarly to mockVariableFinder
$format = $this->getRandomDataType();
$label = "Variable {$i} for Process {$processId}";
$field = "data.var_{$processId}_{$i}";

// 3. Create the VarFinderVariable record linked to the same AssetVariable
VarFinderVariable::create([
'uuid' => (string) Str::uuid(),
'asset_variable_id' => $assetVariable->id,
'data_type' => $format,
'label' => $label,
'field' => $field,
]);
}
}
}

/**
* Test validation for required processIds parameter
*/
Expand DownExpand Up@@ -159,6 +265,9 @@ public function test_saved_search_id_filtering(): void
]);

// Make request with savedSearchId
if (!$this->isVariablesFinderEnabled) {
$this->mockVariableFinder([1], $savedSearch->id);
}
$response = $this->apiCall('GET', '/api/1.1/processes/variables?processIds=1&savedSearchId=' . $savedSearch->id);

$responseData = $response->json();
Expand Down