Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
21054c6
FOUR-32126: INBOX >> First loading is to slow
rodriquelca Jul 28, 2026
c192ab0
feat(FOUR-32800): add v1.1 task completion endpoint with raw SQL path
gproly Aug 28, 2026
6795511
feat(FOUR-32801): improve Jobs execution when the PUT tasks endpoint …
rodriquelca Aug 28, 2026
3fc16e0
style(FOUR-32800): reorder imports in task completion raw service files
gproly Aug 28, 2026
b55a89c
Merge branch 'develop' into feature/FOUR-32800
gproly Sep 2, 2026
05c05a4
perf(FOUR-32947): reduce N+1 queries on requests listing (FOUR-32947)
gproly Sep 4, 2026
a5f315b
FOUR-32800: test compares legacy and optimized
pmPaulis Sep 7, 2026
da5e64f
Merge pull request #9021 from ProcessMaker/bugfix/FOUR-32801
pmPaulis Sep 8, 2026
5e7ca61
Merge pull request #9020 from ProcessMaker/feature/FOUR-32800
pmPaulis Sep 8, 2026
c2ea6f6
feat(FOUR-32800): solving observation about starts
pmPaulis Sep 8, 2026
daafb12
Merge pull request #9046 from ProcessMaker/feature/FOUR-32800
pmPaulis Sep 8, 2026
f600087
feat: optimize notification settings retrieval in Process model
devmiguelangel Sep 10, 2026
b36b825
feat: enhance process retrieval logic by refining relationship handli…
devmiguelangel Sep 10, 2026
a550247
feat: correct query parameters in ProcessMixin to ensure proper data …
devmiguelangel Sep 10, 2026
fdd28de
test: add tests for process listing, including pagination, sorting, a…
devmiguelangel Sep 11, 2026
009273c
Merge branch 'develop' into task/FOUR-32144
gproly Sep 28, 2026
c527057
Merge remote-tracking branch 'origin/develop' into FOUR-32946
devmiguelangel Oct 5, 2026
499436d
fix(FOUR-33645): guard activity execution time metric when ms timesta…
gproly Oct 6, 2026
e314c13
Merge pull request #9045 from ProcessMaker/task/FOUR-32144
pmPaulis Oct 8, 2026
2d97d70
Merge pull request #9056 from ProcessMaker/FOUR-32946
pmPaulis Oct 8, 2026
c1e2d66
Merge pull request #9036 from ProcessMaker/feature/FOUR-32947
pmPaulis Oct 8, 2026
8ab48d6
Merge pull request #9090 from ProcessMaker/bugfix/FOUR-33645
pmPaulis Oct 8, 2026
a7216fd
Merge pull request #8942 from ProcessMaker/feature/FOUR-32126
pmPaulis Oct 8, 2026
9700501
Merge remote-tracking branch 'origin/develop' into epic/FOUR-32144
pmPaulis Oct 21, 2026
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
67 changes: 20 additions & 47 deletions ProcessMaker/Http/Controllers/Api/ProcessController.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,21 @@ public function index(Request $request)
$user = Auth::user();

$orderBy = $this->getRequestSortBy($request, 'name');
if (!str_contains($orderBy[0], '.')) {
$orderBy[0] = 'processes.' . $orderBy[0];
}
$perPage = $this->getPerPage($request);
$include = $this->getRequestInclude($request);
$relationships = array_values(array_diff($include, ['events']));
$status = $request->input('status');
$pmql = $request->input('pmql', '');

$processes = Process::nonSystem()->notArchived()->with($include);
$processes = Process::nonSystem()->notArchived()->with($relationships);
if ($status === 'archived') {
$processes = Process::archived()->with($include);
$processes = Process::archived()->with($relationships);
}
if ($status === 'all') {
$processes = Process::active()->with($include);
$processes = Process::active()->with($relationships);
}

$filter = $request->input('filter', '');
Expand Down Expand Up @@ -188,60 +192,29 @@ public function index(Request $request)
// Get with launchpad
$launchpad = $request->input('launchpad', false);

$processes = $processes->with('events')
->select('processes.*')
$processes = $processes
->with('notification_settings')
->exclude(['bpmn', 'svg'])
->leftJoin(\DB::raw('(select id, uuid, name from process_categories) as category'), 'processes.process_category_id', '=', 'category.id')
->leftJoin(\DB::raw('(select id, uuid, username, lastname, firstname from users) as user'), 'processes.user_id', '=', 'user.id')
->orderBy(...$orderBy)
->get()
->collect();

foreach ($processes as $key => $process) {
// filter the start events that can be used manually (no timer start events);
// TODO: startEvents is not a real property on Process.
// Move below to $process->getManualStartEvents();
$process->startEvents = $process->events->filter(function ($event) {
$eventIsTimerStart = collect($event['eventDefinitions'])
->filter(function ($eventDefinition) {
return $eventDefinition['$type'] == 'timerEventDefinition';
})->count() > 0;

// Filter out web entry start events and email start events
$eventIsWebEntry = false;
$eventIsEmailStart = false;
if (isset($event['config'])) {
$config = json_decode($event['config'], true);
if (isset($config['web_entry']) && $config['web_entry'] !== null) {
$eventIsWebEntry = true;
} elseif (isset($config['email_start']) && $config['email_start'] !== null) {
$eventIsEmailStart = true;
}
}
->paginate($perPage);

return !$eventIsTimerStart && !$eventIsWebEntry && !$eventIsEmailStart;
})->values();
foreach ($processes as $process) {
$process->setAppends(array_values(array_diff($process->getAppends(), ['projects'])));
$process->makeHidden('notification_settings');

// Get the id bookmark related
$process->bookmark_id = Bookmark::getBookmarked($bookmark, $process->id, $user->id);
// Get the launchpad configuration
$process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id);
$process->bookmark_id = $bookmark
? Bookmark::getBookmarked($bookmark, $process->id, $user->id)
: 0;
$process->launchpad = $launchpad
? ProcessLaunchpad::getLaunchpad($launchpad, $process->id)
: null;

$process->case_retention_tier_adjustment_notice = false;
if ($user->is_administrator && config('app.case_retention_policy_enabled')) {
$process->case_retention_tier_adjustment_notice = CaseRetentionTierService::adjustmentNoticeIsActive($process);
}

// Filter all processes that have event definitions (start events like message event, conditional event, signal event, timer event)
if ($request->has('without_event_definitions') && $request->input('without_event_definitions') == 'true') {
$startEvents = $process->events->filter(function ($event) {
return collect($event['eventDefinitions'])->isEmpty();
});
}

// filter only valid executable processes
if (!$process->isValidForExecution()) {
$processes->startEvents = [];
}
}

return new ProcessCollection($processes);
Expand Down
14 changes: 11 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface;
use ProcessMaker\Notifications\ProcessCanceledNotification;
use ProcessMaker\Query\SyntaxError;
use ProcessMaker\RetryProcessRequest;
use ProcessMaker\Repositories\ProcessRequestListingRawRepository;
use ProcessMaker\Traits\ProcessMapTrait;
use Symfony\Component\HttpFoundation\IpUtils;
use Throwable;
Expand Down Expand Up @@ -117,7 +117,7 @@ public function index(Request $request, $getTotal = false, User $user = null)
$query = ProcessRequest::forUser($user);
$includes = $request->input('include', '');
foreach (array_filter(explode(',', $includes)) as $include) {
if (in_array($include, ProcessRequest::$allowedIncludes)) {
if (in_array($include, ProcessRequest::$allowedIncludes) && $include !== 'process') {
$query->with($include);
}
}
Expand Down Expand Up @@ -176,6 +176,7 @@ public function index(Request $request, $getTotal = false, User $user = null)
->withAggregate('processVersion', 'alternative')
->paginate($request->input('per_page', 10));
$total = $response->total();
app(ProcessRequestListingRawRepository::class)->hydrate($response, $includes);
}
} catch (QueryException $e) {
throw $e;
Expand Down Expand Up @@ -900,7 +901,14 @@ public function getRequestsByCase(Request $request, User $user = null)

// Apply ordering only if a valid order_by field is provided
$query->applyOrdering($request);
$response = $query->applyPagination($request);
$response = $query
->select('process_requests.*')
->withAggregate('processVersion', 'alternative')
->applyPagination($request);
app(ProcessRequestListingRawRepository::class)->hydrate(
$response,
$request->input('include', '')
);

// Get activeTasks and participants
$response = $response->map(function ($processRequest) use ($request) {
Expand Down
29 changes: 29 additions & 0 deletions ProcessMaker/Http/Controllers/Api/V1_1/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\ProcessTranslations\TranslationManager;
use ProcessMaker\Services\TaskCompletionRawService;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class TaskController extends Controller
{
public function __construct(
private readonly TaskCompletionRawService $taskCompletionRawService,
) {
}

protected $defaultFields = [
'id',
'element_id',
Expand Down Expand Up @@ -151,4 +158,26 @@ public function showInterstitial($taskId)

return $response;
}

/**
* Complete a task using the raw-query optimized path.
*/
public function update(Request $request, int $taskId)
{
if ($request->input('status') !== 'COMPLETED') {
abort(422, __('Only task completion is supported on this endpoint. Use PUT /api/1.0/tasks/{id} for other updates.'));
}

try {
$task = $this->taskCompletionRawService->completeTask(
$taskId,
json_optimize_decode($request->getContent(), true) ?: [],
$request->user(),
);
} catch (NotFoundHttpException $exception) {
return response()->json(['message' => $exception->getMessage()], 404);
}

return response()->json($task);
}
}
12 changes: 7 additions & 5 deletions ProcessMaker/Http/Resources/ProcessRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ public function toArray($request)
$array['participants'] = $this->participants;
}
if (in_array('activeTasks', $include)) {
$array['active_tasks'] = $this->tokens()
->select(['id', 'element_name', 'status', 'user_id'])
->where('status', 'ACTIVE')
->where('element_type', 'task')
->get();
$array['active_tasks'] = $this->relationLoaded('activeTasks')
? $this->activeTasks
: $this->tokens()
->select(['id', 'element_name', 'status', 'user_id'])
->where('status', 'ACTIVE')
->where('element_type', 'task')
->get();
}
if (in_array('user', $include)) {
$array['user'] = new Users($this->user);
Expand Down
2 changes: 1 addition & 1 deletion ProcessMaker/Jobs/BpmnAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public function handle()
return $response;
}

public function transferInternalContext(BpmnAction $action): void
public function transferInternalContext(self $action): void
{
$action->engine = $this->engine;
$action->instance = $this->instance;
Expand Down
13 changes: 12 additions & 1 deletion ProcessMaker/Jobs/GenerateUserRecommendations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ProcessMaker\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
Expand All @@ -12,17 +13,27 @@
use ProcessMaker\Models\User;
use ProcessMaker\RecommendationEngine;

class GenerateUserRecommendations implements ShouldQueue
class GenerateUserRecommendations implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Seconds the unique lock is held so duplicate dispatches for the same user are dropped.
*/
public int $uniqueFor = 60;

/**
* Create a new job instance.
*/
public function __construct(public int $user_id)
{
}

public function uniqueId(): string
{
return (string) $this->user_id;
}

public function middleware(): array
{
return [(new WithoutOverlapping($this->user_id))->dontRelease()];
Expand Down
35 changes: 19 additions & 16 deletions ProcessMaker/Listeners/BpmnSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,25 @@ public function onActivityCompleted(ActivityCompletedEvent $event)
Log::info('Activity completed: ' . json_encode($token->getProperties()));

// Prometheus Metric: Activity Execution Time
$startTime = $token->created_at_ms;
$completedTime = $token->completed_at_ms;
$executionTime = $completedTime->diffInMilliseconds($startTime);
Metrics::histogramObserve(
'activity_execution_time_seconds',
'Activity Execution Time',
[
'activity_id' => $token->element_id,
'activity_name' => $token->element_name,
'element_type' => $token->element_type,
'process_id' => $token->process_id,
'request_id' => $token->process_request_id,
],
[1, 10, 3600, 86400],
$executionTime,
);
$startTime = $token->created_at_ms ?? $token->created_at;
$completedTime = $token->completed_at_ms ?? $token->completed_at;

if ($startTime && $completedTime) {
$executionTime = $completedTime->diffInMilliseconds($startTime);
Metrics::histogramObserve(
'activity_execution_time_seconds',
'Activity Execution Time',
[
'activity_id' => $token->element_id,
'activity_name' => $token->element_name,
'element_type' => $token->element_type,
'process_id' => $token->process_id,
'request_id' => $token->process_request_id,
],
[1, 10, 3600, 86400],
$executionTime,
);
}

if ($token->element_type == 'task') {
$notifiables = $token->getNotifiables('completed');
Expand Down
14 changes: 8 additions & 6 deletions ProcessMaker/Models/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,13 @@ public function embed()
public function getNotificationsAttribute()
{
$array = [];
$settings = $this->notification_settings->whereNull('element_id');

foreach ($this->requestNotifiableTypes as $notifiable) {
foreach ($this->requestNotificationTypes as $notification) {
$setting = $this->notification_settings()
->whereNull('element_id')
$setting = $settings
->where('notifiable_type', $notifiable)
->where('notification_type', $notification)->get();
->where('notification_type', $notification);

if ($setting->count()) {
$value = true;
Expand All @@ -372,9 +372,7 @@ public function getTaskNotificationsAttribute()
{
$array = [];

$elements = $this->notification_settings()
->whereNotNull('element_id')
->get();
$elements = $this->notification_settings->whereNotNull('element_id');

foreach ($elements->groupBy('element_id') as $group) {
$elementId = $group->first()->element_id;
Expand Down Expand Up @@ -1613,6 +1611,10 @@ public function setProcessCategoryIdAttribute($value)
*/
public function getProcessCategoryIdAttribute($value)
{
if ($this->relationLoaded('categories')) {
return implode(',', $this->categories->pluck('pivot.category_id')->toArray()) ?: $value;
}

return implode(',', $this->categories()->pluck('category_id')->toArray()) ?: $value;
}

Expand Down
2 changes: 1 addition & 1 deletion ProcessMaker/Models/ProcessMakerModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function scopeExclude($query, array $columns)

$columnsToShow = array_diff($this->getTableColumns(), $columns);
$columnsToShow = array_map(function ($column) {
return $this->table . '.' . $column;
return $this->getTable() . '.' . $column;
}, $columnsToShow);

return $query->select($columnsToShow);
Expand Down
37 changes: 37 additions & 0 deletions ProcessMaker/Models/ProcessRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,28 @@ public function shouldBeSearchable()
}
}

/**
* Determine if the model existed in the search index prior to an update.
* Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled.
*
* @return bool
*/
public function wasSearchableBeforeUpdate()
{
return $this->shouldBeSearchable();
}

/**
* Determine if the model existed in the search index prior to deletion.
* Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled.
*
* @return bool
*/
public function wasSearchableBeforeDelete()
{
return $this->shouldBeSearchable();
}

/**
* Get the indexable data array for the model.
*
Expand Down Expand Up @@ -403,6 +425,17 @@ public function tokens()
return $this->hasMany(ProcessRequestToken::class);
}

/**
* Active task tokens for this request.
*/
public function activeTasks()
{
return $this->hasMany(ProcessRequestToken::class)
->select(['id', 'element_name', 'status', 'user_id', 'process_request_id'])
->where('status', 'ACTIVE')
->where('element_type', 'task');
}

/**
* Get the creator/author of this request.
*/
Expand Down Expand Up @@ -1088,6 +1121,10 @@ public function isSystem()

public function getProcessVersionAlternativeAttribute(): string | null
{
if (array_key_exists('process_version_alternative', $this->attributes)) {
return $this->attributes['process_version_alternative'] ?? 'A';
}

return $this->processVersion?->alternative ?? 'A';
}

Expand Down
Loading
Loading