1919use OCA \WorkflowEngine \Helper \ScopeContext ;
2020use OCA \WorkflowEngine \Service \Logger ;
2121use OCA \WorkflowEngine \Service \RuleMatcher ;
22+ use OCP \App \IAppManager ;
2223use OCP \AppFramework \Services \IAppConfig ;
2324use OCP \Cache \CappedMemoryCache ;
2425use OCP \DB \Exception ;
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 */
4962class 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>
0 commit comments