Skip to content

Commit 7eff7fc

Browse files
committed
feat(wfe): add runtime operations
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
1 parent 0f762a0 commit 7eff7fc

5 files changed

Lines changed: 414 additions & 6 deletions

File tree

apps/workflowengine/lib/AppInfo/Application.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace OCA\WorkflowEngine\AppInfo;
88

99
use Closure;
10+
use OC\WorkflowEngine\Events\RegisterRuntimeOperationsEvent;
1011
use OCA\WorkflowEngine\Helper\LogContext;
1112
use OCA\WorkflowEngine\Listener\LoadAdditionalSettingsScriptsListener;
1213
use OCA\WorkflowEngine\Manager;
@@ -40,15 +41,26 @@ public function register(IRegistrationContext $context): void {
4041
}
4142

4243
public function boot(IBootContext $context): void {
44+
$context->injectFn(Closure::fromCallable([$this, 'emitRuntimeEvent']));
4345
$context->injectFn(Closure::fromCallable([$this, 'registerRuleListeners']));
4446
}
4547

48+
private function emitRuntimeEvent(IEventDispatcher $dispatcher, ContainerInterface $container): void {
49+
/** @var Manager $manager */
50+
$manager = $container->get(Manager::class);
51+
$event = new RegisterRuntimeOperationsEvent($manager);
52+
$dispatcher->dispatchTyped($event);
53+
}
54+
4655
private function registerRuleListeners(IEventDispatcher $dispatcher,
4756
ContainerInterface $container,
4857
LoggerInterface $logger): void {
4958
/** @var Manager $manager */
5059
$manager = $container->get(Manager::class);
51-
$configuredEvents = $manager->getAllConfiguredEvents();
60+
$configuredEvents = array_merge_recursive(
61+
$manager->getAllConfiguredEvents(),
62+
$manager->getAllConfiguredRuntimeEvents(),
63+
);
5264

5365
foreach ($configuredEvents as $operationClass => $events) {
5466
foreach ($events as $entityClass => $eventNames) {

apps/workflowengine/lib/Manager.php

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCA\WorkflowEngine\Helper\ScopeContext;
2020
use OCA\WorkflowEngine\Service\Logger;
2121
use OCA\WorkflowEngine\Service\RuleMatcher;
22+
use OCP\App\IAppManager;
2223
use OCP\AppFramework\Services\IAppConfig;
2324
use OCP\Cache\CappedMemoryCache;
2425
use OCP\DB\Exception;
@@ -45,14 +46,51 @@
4546
/**
4647
* @psalm-import-type WorkflowEngineCheck from ResponseDefinitions
4748
* @psalm-import-type WorkflowEngineRule from ResponseDefinitions
49+
*
50+
* @psalm-type RuntimeOperation = array{
51+
* id: string,
52+
* class: class-string<IOperation>,
53+
* name: string,
54+
* checks: string,
55+
* operation: string,
56+
* entity: class-string<IEntity>,
57+
* events: string,
58+
* appId: string,
59+
* runtime: true
60+
* }
4861
*/
4962
class Manager implements IManager {
50-
/** @var array[] */
63+
/** @var array<string, array<string, array<int, WorkflowEngineCheck>>> */
5164
protected array $operations = [];
5265

5366
/** @var array<int, WorkflowEngineCheck> */
5467
protected array $checks = [];
5568

69+
/** @var array<string, array<string, WorkflowEngineCheck>> */
70+
protected array $registeredRuntimeChecks = [];
71+
72+
/**
73+
* @var array<string, array<string, array{
74+
* id: string,
75+
* class: class-string<IOperation>,
76+
* name: string,
77+
* checks: list<string>,
78+
* operation: string,
79+
* entity: class-string<IEntity>,
80+
* events: list<string>,
81+
* }>>
82+
*/
83+
protected array $registeredRuntimeOperations = [];
84+
85+
/**
86+
* @var array<string, array<string, array{
87+
* operationId: string,
88+
* type: int,
89+
* value: string,
90+
* }>>
91+
*/
92+
protected array $registeredRuntimeScopes = [];
93+
5694
/** @var IEntity[] */
5795
protected array $registeredEntities = [];
5896

@@ -77,6 +115,7 @@ public function __construct(
77115
private readonly IEventDispatcher $dispatcher,
78116
private readonly IAppConfig $appConfig,
79117
private readonly ICacheFactory $cacheFactory,
118+
private readonly IAppManager $appManager,
80119
) {
81120
$this->operationsByScope = new CappedMemoryCache(64);
82121
}
@@ -91,7 +130,7 @@ public function getRuleMatcher(): IRuleMatcher {
91130
);
92131
}
93132

94-
public function getAllConfiguredEvents() {
133+
public function getAllConfiguredEvents(): array {
95134
$cache = $this->cacheFactory->createDistributed('flow');
96135
$cached = $cache->get('events');
97136
if ($cached !== null) {
@@ -126,6 +165,30 @@ public function getAllConfiguredEvents() {
126165
return $operations;
127166
}
128167

168+
/**
169+
* Returns the events configured by runtime operations, in the same structure as getAllConfiguredEvents().
170+
*
171+
* @return array<class-string<IOperation>, array<class-string<IEntity>, list<string>>>
172+
*/
173+
public function getAllConfiguredRuntimeEvents(): array {
174+
$eventsByOperationAndEntity = [];
175+
foreach ($this->registeredRuntimeOperations as $appOperations) {
176+
foreach ($appOperations as $operation) {
177+
$operationClass = $operation['class'];
178+
$entityClass = $operation['entity'];
179+
$eventsByOperationAndEntity[$operationClass] ??= [];
180+
$eventsByOperationAndEntity[$operationClass][$entityClass] ??= [];
181+
/** @var list<string> $events */
182+
$events = array_unique(
183+
array_merge($eventsByOperationAndEntity[$operationClass][$entityClass], $operation['events'])
184+
);
185+
$eventsByOperationAndEntity[$operationClass][$entityClass] = $events;
186+
}
187+
}
188+
189+
return $eventsByOperationAndEntity;
190+
}
191+
129192
/**
130193
* @param class-string<IOperation> $operationClass
131194
* @return ScopeContext[]
@@ -167,6 +230,33 @@ public function getAllConfiguredScopesForOperation(string $operationClass): arra
167230
return $this->scopesByOperation[$operationClass];
168231
}
169232

233+
/**
234+
* Gets configured scopes for operations registered at runtime.
235+
*
236+
* @param class-string<IOperation> $operationClass
237+
* @return ScopeContext[]
238+
*/
239+
public function getAllConfiguredScopesForRuntimeOperation(string $operationClass): array {
240+
$scopes = [];
241+
foreach ($this->registeredRuntimeOperations as $appId => $appOperations) {
242+
foreach ($appOperations as $operationId => $operation) {
243+
if ($operation['class'] !== $operationClass) {
244+
continue;
245+
}
246+
247+
$scopeInfo = $this->registeredRuntimeScopes[$appId][$operationId] ?? null;
248+
if ($scopeInfo === null) {
249+
continue;
250+
}
251+
252+
$scope = new ScopeContext($scopeInfo['type'], $scopeInfo['value']);
253+
$scopes[$scope->getHash()] = $scope;
254+
}
255+
}
256+
257+
return $scopes;
258+
}
259+
170260
public function getAllOperations(ScopeContext $scopeContext): array {
171261
if (isset($this->operations[$scopeContext->getHash()])) {
172262
return $this->operations[$scopeContext->getHash()];
@@ -263,6 +353,118 @@ protected function insertOperation(
263353
return $query->getLastInsertId();
264354
}
265355

356+
/**
357+
* Get all operations registered at runtime
358+
*
359+
* @param ScopeContext $scopeContext
360+
* @return array<class-string<IOperation>, list<RuntimeOperation>>
361+
*/
362+
public function getAllRuntimeOperations(ScopeContext $scopeContext): array {
363+
$result = [];
364+
foreach ($this->registeredRuntimeOperations as $appId => $appOperations) {
365+
foreach ($appOperations as $operationId => $operation) {
366+
// scope stored per-app per-operation in registeredRuntimeScopes
367+
$scopeInfo = $this->registeredRuntimeScopes[$appId][$operationId] ?? null;
368+
if ($scopeInfo === null) {
369+
continue;
370+
}
371+
// filter by provided $scopeContext
372+
if ((int)$scopeInfo['type'] !== $scopeContext->getScope()) {
373+
continue;
374+
}
375+
if ($scopeContext->getScope() === IManager::SCOPE_USER && (string)$scopeInfo['value'] !== $scopeContext->getScopeId()) {
376+
continue;
377+
}
378+
379+
$encodedChecks = json_encode($operation['checks']);
380+
$encodedEvents = json_encode($operation['events']);
381+
$row = [
382+
'id' => $operationId, // string uniqid
383+
'class' => $operation['class'],
384+
'name' => $operation['name'],
385+
// encode checks as JSON of hashes to be resolved later
386+
'checks' => $encodedChecks !== false ? $encodedChecks : '',
387+
'operation' => $operation['operation'],
388+
'entity' => $operation['entity'],
389+
'events' => $encodedEvents !== false ? $encodedEvents : '',
390+
'appId' => $appId,
391+
'runtime' => true,
392+
];
393+
$result[$operation['class']][] = $row;
394+
}
395+
}
396+
397+
return $result;
398+
}
399+
400+
/**
401+
* Return operations registered at runtime, which are not persisted in the DB nor shown in the UI.
402+
*
403+
* @param class-string<IOperation> $class
404+
* @param ScopeContext $scopeContext
405+
* @return list<RuntimeOperation>
406+
*/
407+
public function getRuntimeOperations(string $class, ScopeContext $scopeContext): array {
408+
$operations = $this->getAllRuntimeOperations($scopeContext);
409+
410+
return $operations[$class] ?? [];
411+
}
412+
413+
/**
414+
* @param string $appId
415+
* @param class-string<IOperation> $class
416+
* @param string $name
417+
* @param list<WorkflowEngineCheck> $checks
418+
* @param string $operation
419+
* @param class-string<IEntity> $entity
420+
* @param list<class-string<IEntityEvent>> $events
421+
*/
422+
public function addRuntimeOperation(
423+
string $appId,
424+
string $class,
425+
string $name,
426+
array $checks,
427+
string $operation,
428+
ScopeContext $scope,
429+
string $entity,
430+
array $events,
431+
): void {
432+
if (!$this->appManager->isEnabledForAnyone($appId)) {
433+
throw new \InvalidArgumentException("App {$appId} is not enabled");
434+
}
435+
436+
$this->validateOperation($class, $name, $checks, $operation, $scope, $entity, $events);
437+
438+
$checkHashes = [];
439+
foreach ($checks as $check) {
440+
$hash = md5($check['class'] . '::' . $check['operator'] . '::' . $check['value']);
441+
$checkHashes[] = $hash;
442+
$this->registeredRuntimeChecks[$appId] ??= [];
443+
$this->registeredRuntimeChecks[$appId][$hash] ??= $check;
444+
}
445+
446+
$operationId = uniqid($appId, true);
447+
$runtimeOperation = [
448+
'id' => $operationId,
449+
'class' => $class,
450+
'name' => $name,
451+
'checks' => $checkHashes,
452+
'operation' => $operation,
453+
'entity' => $entity,
454+
'events' => $events,
455+
];
456+
$this->registeredRuntimeOperations[$appId] ??= [];
457+
$this->registeredRuntimeOperations[$appId][$operationId] ??= $runtimeOperation;
458+
459+
$runtimeScope = [
460+
'operationId' => $operationId,
461+
'type' => $scope->getScope(),
462+
'value' => $scope->getScopeId(),
463+
];
464+
$this->registeredRuntimeScopes[$appId] ??= [];
465+
$this->registeredRuntimeScopes[$appId][$operationId] ??= $runtimeScope;
466+
}
467+
266468
/**
267469
* @param string $class
268470
* @param string $name
@@ -522,6 +724,22 @@ public function validateOperation(string $class, string $name, array $checks, st
522724
}
523725
}
524726

727+
/**
728+
* @param list<string> $checkHashes
729+
* @param string $appId
730+
* @return array<string, WorkflowEngineCheck> checks indexed by their ID
731+
*/
732+
public function getRuntimeChecks(array $checkHashes, string $appId): array {
733+
$checks = [];
734+
foreach ($checkHashes as $hash) {
735+
if (!isset($this->registeredRuntimeChecks[$appId][$hash])) {
736+
throw new \UnexpectedValueException("Runtime check {$hash} for app {$appId} missing");
737+
}
738+
$checks[$hash] = $this->registeredRuntimeChecks[$appId][$hash];
739+
}
740+
return $checks;
741+
}
742+
525743
/**
526744
* @param int[] $checkIds
527745
* @return array<int, WorkflowEngineCheck>

apps/workflowengine/lib/Service/RuleMatcher.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array {
105105
$operations = [];
106106
foreach ($scopes as $scope) {
107107
$operations = array_merge($operations, $this->manager->getOperations($class, $scope));
108+
$operations = array_merge($operations, $this->manager->getRuntimeOperations($class, $scope));
108109
}
109110

110111
if ($this->entity instanceof IEntity) {
111-
$additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class);
112+
$additionalScopes = $this->manager->getAllConfiguredScopesForOperation($class)
113+
+ $this->manager->getAllConfiguredScopesForRuntimeOperation($class);
112114
foreach ($additionalScopes as $hash => $scopeCandidate) {
113115
if ($scopeCandidate->getScope() !== IManager::SCOPE_USER || in_array($scopeCandidate, $scopes)) {
114116
continue;
@@ -121,6 +123,7 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array {
121123
->setOperation($this->operation);
122124
$this->logger->logScopeExpansion($ctx);
123125
$operations = array_merge($operations, $this->manager->getOperations($class, $scopeCandidate));
126+
$operations = array_merge($operations, $this->manager->getRuntimeOperations($class, $scopeCandidate));
124127
}
125128
}
126129
}
@@ -133,7 +136,11 @@ public function getFlows(bool $returnFirstMatchingOperationOnly = true): array {
133136
}
134137

135138
$checkIds = json_decode($operation['checks'], true);
136-
$checks = $this->manager->getChecks($checkIds);
139+
if (($operation['runtime'] ?? null) === true) {
140+
$checks = $this->manager->getRuntimeChecks($checkIds, $operation['appId']);
141+
} else {
142+
$checks = $this->manager->getChecks($checkIds);
143+
}
137144

138145
foreach ($checks as $check) {
139146
if (!$this->check($check)) {

0 commit comments

Comments
 (0)