Add service and role properties to task and binding nodes in Self-serve Studio Graph tests
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -211,7 +211,8 @@ class selfserve_studio_graph
|
||||
}
|
||||
|
||||
$tasksByScope = [];
|
||||
foreach ($this->sortedRows((array)($config['tasks'] ?? []), ['order_priority', 'id']) as $index => $task) {
|
||||
$taskRows = $this->sortedRows((array)($config['tasks'] ?? []), ['order_priority', 'id']);
|
||||
foreach ($taskRows as $index => $task) {
|
||||
$id = (int)($task['id'] ?? 0);
|
||||
$nodes[] = $this->node('task:' . $id, 'default', $this->entityLabel('task', $id, $task, $lookups), 'task', [
|
||||
'object_id' => $id,
|
||||
@@ -250,6 +251,7 @@ class selfserve_studio_graph
|
||||
}
|
||||
|
||||
$this->appendGatewayNodesAndEdges($nodes, $edges, $gatewayWorkspace);
|
||||
$this->appendTaskServiceEdges($edges, $taskRows, $gatewayWorkspace);
|
||||
|
||||
return [
|
||||
'nodes' => $this->applyLayoutToNodes($nodes, $layout),
|
||||
@@ -541,6 +543,8 @@ class selfserve_studio_graph
|
||||
*/
|
||||
private function appendGatewayNodesAndEdges(array &$nodes, array &$edges, array $workspace): void
|
||||
{
|
||||
$relayServices = $this->relayServicesFromWorkspace($workspace);
|
||||
|
||||
foreach ((array)($workspace['gateways'] ?? []) as $index => $gateway) {
|
||||
if (!is_array($gateway)) {
|
||||
continue;
|
||||
@@ -565,6 +569,13 @@ class selfserve_studio_graph
|
||||
if ($relayId === '') {
|
||||
continue;
|
||||
}
|
||||
$bindingServices = $this->bindingServices($binding, $relayId, $relayServices);
|
||||
if ($bindingServices !== []) {
|
||||
$binding['services'] = $bindingServices;
|
||||
if (trim((string)($binding['role'] ?? '')) === '' && count($bindingServices) === 1) {
|
||||
$binding['role'] = $bindingServices[0];
|
||||
}
|
||||
}
|
||||
$bindingId = 'binding:' . $gatewayId . ':' . $relayId . ':' . $bindingIndex;
|
||||
$nodes[] = $this->node($bindingId, 'default', (string)($binding['label'] ?? ('Relay ' . $relayId)), 'relay_binding', [
|
||||
'object_id' => $relayId,
|
||||
@@ -610,6 +621,39 @@ class selfserve_studio_graph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $edges
|
||||
* @param array<int,array<string,mixed>> $tasks
|
||||
* @param array<string,mixed> $workspace
|
||||
*/
|
||||
private function appendTaskServiceEdges(array &$edges, array $tasks, array $workspace): void
|
||||
{
|
||||
$bindingsByService = [];
|
||||
foreach ($this->gatewayBindingReferences($workspace) as $binding) {
|
||||
foreach ((array)$binding['services'] as $service) {
|
||||
$bindingsByService[$service][] = $binding;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$taskId = (int)($task['id'] ?? 0);
|
||||
if ($taskId <= 0) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->normalizeServiceList($task['services'] ?? []) as $service) {
|
||||
foreach ($bindingsByService[$service] ?? [] as $binding) {
|
||||
$edges[] = $this->edge(
|
||||
'task-service:' . $taskId . ':' . $service . ':' . $binding['gateway_id'] . ':' . $binding['relay_id'] . ':' . $binding['binding_index'],
|
||||
'task:' . $taskId,
|
||||
(string)$binding['node_id'],
|
||||
'task_service',
|
||||
$service
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $operation
|
||||
*/
|
||||
@@ -621,11 +665,11 @@ class selfserve_studio_graph
|
||||
$id = (int)($operation['id'] ?? $data['id'] ?? 0);
|
||||
|
||||
if ($action === 'connect') {
|
||||
$this->applyConnection((string)($operation['source'] ?? ''), (string)($operation['target'] ?? ''), false);
|
||||
$this->applyConnection($departmentId, (string)($operation['source'] ?? ''), (string)($operation['target'] ?? ''), false);
|
||||
return;
|
||||
}
|
||||
if ($action === 'disconnect') {
|
||||
$this->applyConnection((string)($operation['source'] ?? ''), (string)($operation['target'] ?? ''), true);
|
||||
$this->applyConnection($departmentId, (string)($operation['source'] ?? ''), (string)($operation['target'] ?? ''), true);
|
||||
return;
|
||||
}
|
||||
if ($action === 'reorder') {
|
||||
@@ -884,7 +928,7 @@ class selfserve_studio_graph
|
||||
}
|
||||
}
|
||||
|
||||
private function applyConnection(string $source, string $target, bool $disconnect): void
|
||||
private function applyConnection(int $departmentId, string $source, string $target, bool $disconnect): void
|
||||
{
|
||||
[$sourceType, $sourceId] = $this->parseNodeId($source);
|
||||
[$targetType, $targetId] = $this->parseNodeId($target);
|
||||
@@ -892,6 +936,23 @@ class selfserve_studio_graph
|
||||
throw new \RuntimeException('Invalid connection endpoints.');
|
||||
}
|
||||
|
||||
if ($sourceType === 'task' && $targetType === 'binding') {
|
||||
$service = $this->serviceForBindingNode($departmentId, $target);
|
||||
if ($service === '') {
|
||||
throw new \RuntimeException('Relay binding has no service role to connect to the task.');
|
||||
}
|
||||
$this->updateTaskServiceConnection($departmentId, (int)$sourceId, $service, $disconnect);
|
||||
return;
|
||||
}
|
||||
if ($sourceType === 'binding' && $targetType === 'task') {
|
||||
$service = $this->serviceForBindingNode($departmentId, $source);
|
||||
if ($service === '') {
|
||||
throw new \RuntimeException('Relay binding has no service role to connect to the task.');
|
||||
}
|
||||
$this->updateTaskServiceConnection($departmentId, (int)$targetId, $service, $disconnect);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($sourceType === 'condition' && $targetType === 'question') {
|
||||
db::getPDO()->prepare('UPDATE department_selfserve_questions SET condition_id = :condition_id WHERE id = :id')->execute([
|
||||
':condition_id' => $disconnect ? null : (int)$sourceId,
|
||||
@@ -1184,13 +1245,183 @@ class selfserve_studio_graph
|
||||
*/
|
||||
private function normalizeTaskPayload(array $task): array
|
||||
{
|
||||
foreach (['services', 'buttons'] as $field) {
|
||||
if (isset($task[$field]) && is_string($task[$field])) {
|
||||
$decoded = json_decode((string)$task[$field], true);
|
||||
$task[$field] = is_array($decoded) ? $decoded : [];
|
||||
$task['services'] = $this->normalizeServiceList($task['services'] ?? []);
|
||||
$task['buttons'] = $this->normalizeArrayPayload($task['buttons'] ?? []);
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $workspace
|
||||
* @return array<string,array<int,string>>
|
||||
*/
|
||||
private function relayServicesFromWorkspace(array $workspace): array
|
||||
{
|
||||
$servicesByRelay = [];
|
||||
foreach ((array)($workspace['lanes'] ?? []) as $lane) {
|
||||
if (!is_array($lane)) {
|
||||
continue;
|
||||
}
|
||||
foreach ((array)($lane['relay_slots'] ?? []) as $slot) {
|
||||
if (!is_array($slot)) {
|
||||
continue;
|
||||
}
|
||||
$relayId = trim((string)($slot['relay_id'] ?? ''));
|
||||
$service = $this->normalizeServiceName($slot['slot'] ?? $slot['role'] ?? $slot['service'] ?? '');
|
||||
if ($relayId === '' || $service === '') {
|
||||
continue;
|
||||
}
|
||||
$servicesByRelay[$relayId][$service] = true;
|
||||
}
|
||||
}
|
||||
return $task;
|
||||
|
||||
return array_map(static fn(array $services): array => array_keys($services), $servicesByRelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $binding
|
||||
* @param array<string,array<int,string>> $relayServices
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function bindingServices(array $binding, string $relayId, array $relayServices): array
|
||||
{
|
||||
$services = [];
|
||||
foreach (['role', 'service', 'slot'] as $field) {
|
||||
$service = $this->normalizeServiceName($binding[$field] ?? '');
|
||||
if ($service !== '') {
|
||||
$services[$service] = true;
|
||||
}
|
||||
}
|
||||
foreach ($this->normalizeServiceList($binding['services'] ?? []) as $service) {
|
||||
$services[$service] = true;
|
||||
}
|
||||
foreach ((array)($relayServices[$relayId] ?? []) as $service) {
|
||||
$normalized = $this->normalizeServiceName($service);
|
||||
if ($normalized !== '') {
|
||||
$services[$normalized] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($services);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $workspace
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function gatewayBindingReferences(array $workspace): array
|
||||
{
|
||||
$relayServices = $this->relayServicesFromWorkspace($workspace);
|
||||
$references = [];
|
||||
foreach ((array)($workspace['gateways'] ?? []) as $gateway) {
|
||||
if (!is_array($gateway)) {
|
||||
continue;
|
||||
}
|
||||
$gatewayId = (int)($gateway['id'] ?? 0);
|
||||
if ($gatewayId <= 0) {
|
||||
continue;
|
||||
}
|
||||
foreach ((array)($gateway['bindings'] ?? []) as $bindingIndex => $binding) {
|
||||
if (!is_array($binding)) {
|
||||
continue;
|
||||
}
|
||||
$relayId = trim((string)($binding['relay_id'] ?? ''));
|
||||
if ($relayId === '') {
|
||||
continue;
|
||||
}
|
||||
$services = $this->bindingServices($binding, $relayId, $relayServices);
|
||||
if ($services === []) {
|
||||
continue;
|
||||
}
|
||||
$references[] = [
|
||||
'gateway_id' => $gatewayId,
|
||||
'relay_id' => $relayId,
|
||||
'binding_index' => (int)$bindingIndex,
|
||||
'node_id' => 'binding:' . $gatewayId . ':' . $relayId . ':' . $bindingIndex,
|
||||
'services' => $services,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $references;
|
||||
}
|
||||
|
||||
private function serviceForBindingNode(int $departmentId, string $nodeId): string
|
||||
{
|
||||
$workspace = $this->buildGatewayWorkspace($departmentId);
|
||||
foreach ($this->gatewayBindingReferences($workspace) as $binding) {
|
||||
if ((string)$binding['node_id'] === $nodeId) {
|
||||
return (string)($binding['services'][0] ?? '');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function updateTaskServiceConnection(int $departmentId, int $taskId, string $service, bool $disconnect): void
|
||||
{
|
||||
if ($taskId <= 0 || $service === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = db::getPDO();
|
||||
$statement = $pdo->prepare(
|
||||
'SELECT services FROM department_selfserve_tasks WHERE id = :id AND department IN (0, :department) LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
':id' => $taskId,
|
||||
':department' => $departmentId,
|
||||
]);
|
||||
$row = $statement->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!is_array($row)) {
|
||||
throw new \RuntimeException('Task is not available in the selected department.');
|
||||
}
|
||||
|
||||
$services = $this->normalizeServiceList($row['services'] ?? []);
|
||||
$serviceSet = array_fill_keys($services, true);
|
||||
if ($disconnect) {
|
||||
unset($serviceSet[$service]);
|
||||
} else {
|
||||
$serviceSet[$service] = true;
|
||||
}
|
||||
|
||||
$pdo->prepare(
|
||||
'UPDATE department_selfserve_tasks SET services = :services WHERE id = :id AND department IN (0, :department)'
|
||||
)->execute([
|
||||
':services' => $this->jsonArray(array_keys($serviceSet)),
|
||||
':id' => $taskId,
|
||||
':department' => $departmentId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,mixed>
|
||||
*/
|
||||
private function normalizeArrayPayload(mixed $value): array
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
$value = is_array($decoded) ? $decoded : array_filter(array_map('trim', explode(',', $value)), static fn(string $item): bool => $item !== '');
|
||||
}
|
||||
return is_array($value) ? array_values($value) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function normalizeServiceList(mixed $value): array
|
||||
{
|
||||
$services = [];
|
||||
foreach ($this->normalizeArrayPayload($value) as $entry) {
|
||||
$service = $this->normalizeServiceName($entry);
|
||||
if ($service !== '') {
|
||||
$services[$service] = true;
|
||||
}
|
||||
}
|
||||
return array_keys($services);
|
||||
}
|
||||
|
||||
private function normalizeServiceName(mixed $value): string
|
||||
{
|
||||
return strtoupper(trim((string)$value));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1307,11 +1538,14 @@ class selfserve_studio_graph
|
||||
if ($gatewayId <= 0 || $relayId === '') {
|
||||
continue;
|
||||
}
|
||||
$services = $this->bindingServices($binding, $relayId, []);
|
||||
$rows[] = [
|
||||
'id' => $gatewayId . ':' . $relayId . ':' . $index,
|
||||
'label' => (string)($binding['label'] ?? ('Gateway ' . $gatewayId . ' relay ' . $relayId)),
|
||||
'gateway_id' => $gatewayId,
|
||||
'relay_id' => $relayId,
|
||||
'role' => (string)($binding['role'] ?? ''),
|
||||
'services' => $services,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ it('serializes questions, conditions, tasks, scopes, and gateways into one graph
|
||||
['id' => 20, 'condition_id' => 10, 'type' => 'IS_TRUE', 'object_type' => 'question', 'object_id' => 1, 'name' => 'Mirror answer'],
|
||||
],
|
||||
'tasks' => [
|
||||
['id' => 30, 'task' => 'Fold mirrors', 'gate_type' => 'QUESTION', 'gate_ref_id' => 1, 'lane' => 7, 'product' => 3, 'department' => 2, 'order_priority' => 1],
|
||||
['id' => 30, 'task' => 'Fold mirrors', 'gate_type' => 'QUESTION', 'gate_ref_id' => 1, 'lane' => 7, 'product' => 3, 'department' => 2, 'order_priority' => 1, 'services' => ['MACHINE']],
|
||||
],
|
||||
], [
|
||||
'lookups' => [
|
||||
@@ -50,7 +50,7 @@ it('serializes questions, conditions, tasks, scopes, and gateways into one graph
|
||||
'label' => 'Gateway A',
|
||||
'status' => 'ONLINE',
|
||||
'bindings' => [
|
||||
['relay_id' => 'relay-1', 'label' => 'Machine relay'],
|
||||
['relay_id' => 'relay-1', 'label' => 'Machine relay', 'role' => 'MACHINE'],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -84,7 +84,20 @@ it('serializes questions, conditions, tasks, scopes, and gateways into one graph
|
||||
expect($edgeIds)->toContain('scope:vehicle_type:3:condition:10');
|
||||
expect($edgeIds)->toContain('scope:vehicle_type:3:task:30');
|
||||
expect($edgeIds)->toContain('gateway-binding:50:relay-1:0');
|
||||
expect($edgeIds)->toContain('task-service:30:MACHINE:50:relay-1:0');
|
||||
expect($edgeIds)->toContain('relay-lane:relay-1:7:MACHINE');
|
||||
|
||||
$taskNode = array_values(array_filter(
|
||||
$graph['nodes'],
|
||||
static fn(array $node): bool => ($node['id'] ?? null) === 'task:30'
|
||||
))[0] ?? null;
|
||||
$bindingNode = array_values(array_filter(
|
||||
$graph['nodes'],
|
||||
static fn(array $node): bool => ($node['id'] ?? null) === 'binding:50:relay-1:0'
|
||||
))[0] ?? null;
|
||||
|
||||
expect($taskNode['data']['raw']['services'] ?? [])->toBe(['MACHINE']);
|
||||
expect($bindingNode['data']['raw']['services'] ?? [])->toBe(['MACHINE']);
|
||||
});
|
||||
|
||||
it('derives studio vehicle type lookup rows from selectable wash products', function (): void {
|
||||
|
||||
Reference in New Issue
Block a user