Skip to content

Track Actions done on Projects#1486

Open
FyreByrd wants to merge 19 commits intodevelopfrom
feature/project-transitions
Open

Track Actions done on Projects#1486
FyreByrd wants to merge 19 commits intodevelopfrom
feature/project-transitions

Conversation

@FyreByrd
Copy link
Contributor

@FyreByrd FyreByrd commented Mar 24, 2026

Changes:

  • new DB model ProjectActions
model ProjectActions {
  Id         Int
  ProjectId  Int
  UserId     Int
  DateAction DateTime
  ActionType Int
  Action     String // Archive/Reactivate, Upload/Download, Add/Remove, Owner/Group, Field Name
  Value      String? // Store.BuildEnginePublisherId, Reviewer.Email, Field Value
  ExternalId Int? // Variable link to ProductDefinitions, Users, and Groups
}
  • Tracked Actions
    • Archive/Reactivate, relocated from ProductTransitions, still showed in ProductDetails
    • Upload/Download, relocated from ProductTransitions, still showed in ProductDetails
    • Claim/Reassign Owner
    • Reassign Group
    • Add/Remove Product
    • Add/Remove Author
    • Add/Remove Reviewer
    • Edit Field
  • New tracking in ProductTransitions
    • Edit Properties
    • Transfer between Stores
  • Fixed bug with ProductTransitions and Owner change in archived projects
  • Rename Details to History for ProductTransitions and now ProjectActions
  • Add batched migration to create ProjectActions from relevant ProductTransitions

Screenshots:
Project page
Screenshot 2026-03-24 at 10 36 39 AM

ProjectActions
Screenshot 2026-03-24 at 10 37 26 AM

ProductTransitions
Screenshot 2026-03-24 at 10 37 53 AM

Summary by CodeRabbit

  • New Features

    • Project history modal showing detailed action entries (who, when, specifics) and a merged timeline of transitions and actions
    • New "Transfer" and "Update" transition types surfaced in timelines and details
  • Improvements

    • Unified timeline merging product transitions and project actions
    • Improved user display with Id fallback when names are missing
    • Updated icons for transfer/update/project actions and a details toolbar to open project history
  • Localization

    • "Details" label renamed to "History" and new labels for transfer/update and action group added

@FyreByrd FyreByrd requested a review from chrisvire March 24, 2026 15:38
@FyreByrd FyreByrd added the DB Migration This PR makes changes to the database schema label Mar 24, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds a ProjectActions audit model and migration; records project-level action entries across server handlers; surfaces actions in UI with new components and modal; migrates legacy product transitions into project actions; updates enums, icons, locales, SSE enqueueing, and related job/migration steps.

Changes

Cohort / File(s) Summary
Database schema & migration
src/lib/prisma/schema.prisma, src/lib/prisma/migrations/35_project_actions/migration.sql
Add new ProjectActions Prisma model with relations to Projects and Users, timestamps, optional value/external id, cascade FK, and indexes.
Prisma enums & maps
src/lib/prisma.ts
Add ProjectActionType, ProjectActionString, ProjectActionValue; add Transfer and Update to ProductTransitionType and mark some legacy members as deprecated (comments).
Server DB handlers
src/lib/server/database/ProjectActions.ts, src/lib/server/database/index.ts, src/lib/server/database/Products.ts
New handlers create/createMany for projectActions and export via DatabaseWrites; Products.update success now returns id (`Promise<string
Server logic & migration
src/lib/server/job-executors/system.ts, src/lib/server/job-executors/build.ts, src/lib/server/job-executors/userTasks.ts, src/lib/server/bullmq/BullWorker.ts
Add Migrate Project Actions migration step and scheduler entry; migration converts product transitions to project actions and enqueues SSE updates; build executor writes ProductTransitionType.Update on retry-success; minor deletion-condition tweak in userTasks.
Project server & SSE
src/lib/projects/server.ts, src/lib/projects/sse.ts
doProjectAction now writes projectActions; getProjectDetails selects ProjectActions and returns actionParams (users, groups, prodDefs) for UI rendering.
Routes: project & transfer flows
src/routes/.../projects/[id]/+page.server.ts, src/routes/.../projects/[id]/edit/+page.server.ts, src/routes/.../settings/stores/transfer/+page.server.ts, src/routes/(unauthenticated)/api/projects/[id]/token/+server.ts
Multiple handlers now create projectActions for archive/reactivate/claim/add/remove/edit/transfer/access; project edit action persists field-change audit via createMany; token endpoint records access via projectActions; store transfer writes Transfer transitions.
Product details & product card UI
src/lib/products/components/ProductDetails.svelte, src/routes/.../projects/[id]/ProductCard.svelte
ProductDetails accepts projectActions prop, merges/sorts product transitions and project actions into a single chronology; ProductCard passes projectActions through.
Project UI components
src/lib/projects/components/ProjectActionEntry.svelte, src/lib/projects/components/ProjectDetails.svelte, src/lib/projects/components/ProjectActionMenu.svelte
Add ProjectActionEntry (renders action rows), ProjectDetails modal with showProjectDetails() helper; minor dropdown styling tweak in ProjectActionMenu.
Icons & locales
src/lib/icons/index.ts, src/lib/locales/en-US.json, src/lib/locales/es-419.json, src/lib/locales/fr-FR.json
Replace getAccessIcon with getProjectActionIcon, update getTransitionIcon to call it and add Transfer/Update; update IconType. Locales: products_details → "History", add labels for types 9 (Transfer) and 10 (Update), and project action keys (project_details_action, project_action_group).
SSE / client selection changes
src/routes/(authenticated)/workflow-instances/[product_id]/+page.server.ts
Transition User selection now includes Id in addition to Name for consistent fallbacks/display.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Browser
    participant Server as AppServer
    participant DB as Database (Prisma)
    participant Queue as BullMQ (SvelteSSE)
    participant SSE as SSE Broadcaster

    User->>Client: submit project change (edit/owner/claim/archive/access)
    Client->>Server: POST action/request
    Server->>DB: update project / create ProjectActions
    DB-->>Server: result / created action(s)
    Server->>Queue: enqueue SvelteSSE_UpdateProject for projectId
    Queue-->>SSE: trigger project update broadcast
    SSE->>Client: stream updated project details (incl. ProjectActions)
    Client->>Client: render action history/modal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #1470 — overlaps icon/transition-type changes and related enum adjustments in icons and transition helpers.
  • #1477 — related to migration-step additions and scheduling updates in the system migration/worker code.
  • #1430 — related migration-step changes for converting legacy product transitions and migration orchestration.

Suggested reviewers

  • chrisvire
  • 7dev7urandom

"🐰
I hopped through tables, rows, and time,
I stitched the actions, one by one, line by line,
History now blooms where changes once hid,
Project trails pranced out — neat and well amid! 🥕"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title "Track Actions done on Projects" directly and clearly summarizes the main change: adding comprehensive tracking of project-related actions via a new ProjectActions database model and related UI/infrastructure changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/project-transitions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/projects/server.ts (1)

123-184: ⚠️ Potential issue | 🔴 Critical

Keep project state, audit rows, and task side effects on the same confirmed success path.

DatabaseWrites.projects.update() can return false, but the archive/reactivate branches still create a ProjectActions row and enqueue task mutations even when that happens. DatabaseWrites.projectActions.create() can also return false, and that result is ignored in all three branches. That makes it possible for project state, audit history, and user-task churn to drift out of sync.

🧩 Minimum guard to add immediately
-    await DatabaseWrites.projects.update(project.Id, {
+    const updated = await DatabaseWrites.projects.update(project.Id, {
       DateArchived: timestamp
     });
-    await DatabaseWrites.projectActions.create({
+    if (!updated) return;
+
+    const action = await DatabaseWrites.projectActions.create({
       ProjectId: project.Id,
       UserId: security.userId,
       DateAction: timestamp,
       ActionType: ProjectActionType.Archival,
       Action: ProjectActionString.Archive
     });
+    if (!action) return;

Please apply the same guard to reactivate and claim, and ideally move the project update + audit insert into one transactional helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/projects/server.ts` around lines 123 - 184,
DatabaseWrites.projects.update(...) can return false but the code proceeds to
create ProjectActions and enqueue UserTasks regardless, and
DatabaseWrites.projectActions.create(...) is also unchecked; ensure archive,
reactivate and claim only perform the audit insert and enqueue the UserTasks
after a successful DatabaseWrites.projects.update(project.Id, ...) (check the
returned success boolean), and also verify the projectActions create result
before enqueuing; preferably move the update + audit-create + queue-add into a
transactional helper (e.g., a new helper that wraps
DatabaseWrites.projects.update, DatabaseWrites.projectActions.create and
getQueues().UserTasks.add) so that the project state, audit rows, and task side
effects are applied only on confirmed success.
src/routes/(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts (1)

114-154: ⚠️ Potential issue | 🟠 Major

Make the store move and transition write atomic.

All products.update() calls complete before any productTransitions.createMany() runs. If the second phase fails, the products stay moved but the transfer history is missing. This should be a single transactional write path, or at least one dedicated helper that performs both steps together.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/routes/`(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts
around lines 114 - 154, The current flow updates products with
DatabaseWrites.products.update first, then separately creates ProductTransition
rows with DatabaseWrites.productTransitions.createMany, which can leave products
moved without transfer history if the second step fails; wrap the
update+transition for each project in a single transactional operation (or add a
dedicated helper like transferProducts or
DatabaseWrites.transferProducts/transferProjectProducts) that within one
transaction updates the Product.StoreId(s) (formerly using products.update) and
inserts the corresponding productTransitions records (formerly createMany) using
the same transferDate and event.locals.security.userId, and ensure the
transaction rolls back on any error so both steps are atomic; locate logic
around the successes/grouped variables and replace the separate Promise.all
calls with the new transactional helper per grouped project.
🧹 Nitpick comments (5)
src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte (1)

252-256: Hoist the project-action filter out of the product loop.

Every card recomputes the same scan over projectData.project.ProjectActions. Deriving that array once will keep render cost flat as history grows and avoid reallocating identical projectActions arrays per product.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/`(authenticated)/projects/[id=idNumber]/+page.svelte around lines
252 - 256, The code repeatedly filters projectData.project.ProjectActions inside
the product/card rendering, causing redundant recomputation; compute a single
derived array once (e.g., const filteredProjectActions =
projectData.project.ProjectActions.filter(pa => pa.ActionType ===
ProjectActionType.Access || pa.ActionType === ProjectActionType.Archival))
outside the product loop or card component and pass that filteredProjectActions
into each card via the projectActions prop instead of calling .filter(...) per
product.
src/lib/projects/sse.ts (1)

325-377: Precompute unique action lookup IDs once and fetch actionParams in parallel.

project.ProjectActions is filtered/mapped three times here, and each findMany() waits for the previous one. A single pre-pass with Sets plus Promise.all() will trim work on every SSE refresh. Based on learnings: FyreByrd prefers to optimize by creating Sets at the caller level for better performance and separation of concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/projects/sse.ts` around lines 325 - 377, project.ProjectActions is
being filtered and mapped three times and the three DatabaseReads.findMany calls
inside actionParams are executed sequentially; refactor by precomputing three
Sets of unique ExternalId values (e.g., authorIds, ownerGroupIds, productDefIds)
from project.ProjectActions once, then call DatabaseReads.users.findMany,
DatabaseReads.groups.findMany, and DatabaseReads.productDefinitions.findMany in
parallel using Promise.all and the precomputed Sets in the respective where.in
clauses so actionParams is built from the parallel results.
src/lib/server/database/ProjectActions.ts (1)

15-17: Silent exception swallowing hinders debugging.

Both create and createMany catch exceptions and return false without logging. When callers don't check the return value (as seen in src/lib/projects/server.ts:125-145), failures go completely unnoticed.

Consider logging the error before returning:

♻️ Proposed fix
   } catch {
+    console.error('Failed to create projectAction(s)');
     return false;
   }

Also applies to: 33-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/server/database/ProjectActions.ts` around lines 15 - 17, The catch
blocks in ProjectActions.ts (in the create and createMany functions) silently
swallow exceptions and return false; change those catches to accept the error
(catch (err)) and log the error before returning so failures are visible; use
the module's established logger (or console.error if none exists) to emit a
contextual message including the function name (e.g., "ProjectActions.create" /
"ProjectActions.createMany") and the caught error, then return false as before.
src/lib/prisma/schema.prisma (1)

301-313: Consider adding indexes on foreign key columns.

The ProjectActions model lacks indexes on ProjectId and UserId. Given that queries will frequently filter by project (e.g., loading action history for a project page), performance could degrade as the table grows.

🗃️ Proposed schema addition
   User    Users    `@relation`(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProjectActions_Users_UserId")
+
+  @@index([ProjectId], map: "IX_ProjectActions_ProjectId")
+  @@index([UserId], map: "IX_ProjectActions_UserId")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/prisma/schema.prisma` around lines 301 - 313, Add indexes on the
foreign key columns in the ProjectActions model: add @@index([ProjectId]) and
@@index([UserId]) to the ProjectActions model so queries filtering by ProjectId
or UserId use indexes; update the Prisma schema (model ProjectActions)
accordingly and run prisma migrate to generate/apply the migration.
src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts (1)

214-226: Consider moving the store lookup before the queue job.

The BuildEnginePublisherId lookup (lines 219-224) occurs after the product creation job is queued. If the store doesn't exist (unlikely given earlier validation), Value would be undefined. Consider fetching this data earlier alongside the repository check, or add a fallback value.

♻️ Optional: Consolidate store lookup with earlier validation
     const checkRepository = await DatabaseReads.projects.findUnique({
       where: {
         Id: parseInt(event.params.id)
       },
       select: {
         RepositoryUrl: true
       }
     });
     if (!checkRepository?.RepositoryUrl) {
       return error(400, 'Project Repository not Yet Initialized');
     }
+    const store = await DatabaseReads.stores.findFirst({
+      where: { Id: form.data.storeId },
+      select: { BuildEnginePublisherId: true }
+    });
     await getQueues().Products.add(`Create Product for Project #${event.params.id}`, {
       type: BullMQ.JobType.Product_CreateLocal,
       projectId,
       productDefinitionId: form.data.productDefinitionId,
       storeId: form.data.storeId
     });
     await DatabaseWrites.projectActions.create({
       ProjectId: projectId,
       UserId: event.locals.security.userId,
       ActionType: ProjectActionType.Product,
       Action: ProjectActionString.AddProduct,
-      Value: (
-        await DatabaseReads.stores.findFirst({
-          where: { Id: form.data.storeId },
-          select: { BuildEnginePublisherId: true }
-        })
-      )?.BuildEnginePublisherId,
+      Value: store?.BuildEnginePublisherId,
       ExternalId: form.data.productDefinitionId
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/`(authenticated)/projects/[id=idNumber]/+page.server.ts around
lines 214 - 226, Move the BuildEnginePublisherId lookup out of the
DatabaseWrites.projectActions.create call so it is fetched before queueing the
job (e.g. alongside the earlier repository/store validation) using
DatabaseReads.stores.findFirst to assign a local variable
(BuildEnginePublisherId) and pass that variable into
DatabaseWrites.projectActions.create; alternatively, if you prefer minimal
change, perform the findFirst just before calling
DatabaseWrites.projectActions.create and provide a sensible fallback (empty
string or null) for Value when BuildEnginePublisherId is undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/locales/fr-FR.json`:
- Line 235: Update the French locale JSON to replace the English values with
proper French translations for the affected keys: change the value for
"products_details" from "History" to "Historique", replace the entry currently
containing "Transfer Store" with a French equivalent (e.g., "Transfert de
magasin"), replace the entry currently containing "Update Properties" with a
French equivalent (e.g., "Mettre à jour les propriétés"), and replace the entry
containing "Transfer Group" with a French equivalent (e.g., "Transférer le
groupe"); locate these by the existing JSON keys/strings in
src/lib/locales/fr-FR.json and update only the string values, preserving JSON
structure and commas.

In `@src/lib/prisma/migrations/35_project_actions/migration.sql`:
- Around line 2-19: The migration creates the ProjectActions table with foreign
keys ProjectId and UserId but doesn't add indexes on those referencing columns,
which will cause full-table scans as the audit log grows; update the migration
to create indexes on "ProjectActions"("ProjectId") and
"ProjectActions"("UserId") (e.g., names like IX_ProjectActions_ProjectId and
IX_ProjectActions_UserId) so reads filtered by project or user use the indexes,
ensuring the index creation statements are added alongside the table and FK
ALTERs in the migration.sql.

In `@src/lib/products/components/ProductDetails.svelte`:
- Around line 270-271: The ProjectActionEntry instances in ProductDetails.svelte
are missing the optional prodDefs, users, and groups props so action details
(product definition names, user/group names) render incomplete; update
ProductDetails.svelte's export props (the component Props) to accept prodDefs,
users, and groups (matching the types used by ProjectActionEntry) and forward
them into every ProjectActionEntry usage (e.g., <ProjectActionEntry
act={transition} compact={false} prodDefs={prodDefs} users={users}
groups={groups} /> and the other occurrence) so the child can resolve names
correctly.

In `@src/lib/projects/components/ProjectActionEntry.svelte`:
- Around line 114-123: The current render calls m[act.Value as ValidI13nKey]
when useI18n is true, which can throw if act.Value is not a valid i18n key;
update the extraBox branch (and the compact variant) to defensively check that
act.Value exists as a key and that m[act.Value] is a callable translation before
invoking it, and otherwise pass a safe fallback (e.g., act.Value or a default
translation string) into TaskComment; refer to symbols extraBox, useI18n, act,
m, TaskComment, and ValidI13nKey when locating the code to change.
- Around line 91-92: The template can render "User `#null`" when act.ExternalId is
null; update the fallback in ProjectActionEntry.svelte (the expression using act
and users) to check for a non-null ExternalId before interpolating it: if
users.find(...) fails and act.ExternalId is not null/undefined show `User
#<id>`, otherwise show a safe label like "Unknown user" (or just omit the
"#<id>") so that act.ExternalId is never stringified as "null"; modify the
expression that references act.ExternalId and users?.find(...) accordingly.
- Around line 52-53: ProjectActionType.Access currently renders raw action
strings; add corresponding keys to the ProjectActionString enum for the Access
values (e.g., Upload, Download, use) and update ProjectActionEntry.svelte so the
Access branch uses the i18n lookup like the others: replace the raw {act.Action}
rendering with m[act.Action as ValidI13nKey]({} as never). Ensure enum keys
match the act.Action values and that ValidI13nKey covers them.

In `@src/lib/server/job-executors/system.ts`:
- Around line 733-746: The current mapping uses a non-null assertion on
pt.UserId and writes raw workflow names into ProjectAction.Action; instead,
update the mapping in the p.Products[0].ProductTransitions -> ProjectAction
creation so that (1) you do not use pt.UserId! — either skip creating the
ProjectAction when pt.UserId is null or explicitly set UserId to null/undefined
only if your storage accepts that (preferably filter out transitions with
pt.UserId == null before mapping), and (2) replace the inline pt.InitialState
use for ProjectAccess transitions with a translation to a valid
ProjectActionString: add a small mapping (e.g., map InitialState values like
"Product Build"/"Product Publish" to concrete ProjectActionString constants or
fall back to a generic ProjectActionString.Access) and use that mapped value
when pt.TransitionType === ProductTransitionType.ProjectAccess; keep the
existing logic for Archival/Reactive branches unchanged and ensure DateAction
uses pt.DateTransition as before.
- Around line 730-756: The createMany and deleteMany calls inside the
projects.map block (DatabaseWrites.projectActions.createMany and
DatabaseWrites.productTransitions.deleteMany) must be executed atomically to
avoid partial migration state; wrap both operations for each project in a single
transaction (use the existing transaction helper used elsewhere, e.g.,
prismaInternal.$transaction or the DatabaseWrites transaction API) so that if
deleteMany fails the createMany is rolled back, or alternatively check the
result of createMany and abort/rollback before calling deleteMany; ensure the
transaction encloses both the creation payload (built from
p.Products[0].ProductTransitions) and the delete filter (transitionFilter with
Product: { ProjectId: p.Id }) for each project.

In
`@src/routes/`(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts:
- Around line 88-90: The POST action currently queries Products with where: {
Id: { in: form.data.products }, Project: { OrganizationId: orgId } } but does
not filter out archived projects; update the Products where predicate to mirror
the load() filter by adding Project: { OrganizationId: orgId, DateArchived: null
} (i.e., include DateArchived: null alongside OrganizationId) so the action only
operates on products belonging to active projects and cannot transfer products
from archived projects.

---

Outside diff comments:
In `@src/lib/projects/server.ts`:
- Around line 123-184: DatabaseWrites.projects.update(...) can return false but
the code proceeds to create ProjectActions and enqueue UserTasks regardless, and
DatabaseWrites.projectActions.create(...) is also unchecked; ensure archive,
reactivate and claim only perform the audit insert and enqueue the UserTasks
after a successful DatabaseWrites.projects.update(project.Id, ...) (check the
returned success boolean), and also verify the projectActions create result
before enqueuing; preferably move the update + audit-create + queue-add into a
transactional helper (e.g., a new helper that wraps
DatabaseWrites.projects.update, DatabaseWrites.projectActions.create and
getQueues().UserTasks.add) so that the project state, audit rows, and task side
effects are applied only on confirmed success.

In
`@src/routes/`(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts:
- Around line 114-154: The current flow updates products with
DatabaseWrites.products.update first, then separately creates ProductTransition
rows with DatabaseWrites.productTransitions.createMany, which can leave products
moved without transfer history if the second step fails; wrap the
update+transition for each project in a single transactional operation (or add a
dedicated helper like transferProducts or
DatabaseWrites.transferProducts/transferProjectProducts) that within one
transaction updates the Product.StoreId(s) (formerly using products.update) and
inserts the corresponding productTransitions records (formerly createMany) using
the same transferDate and event.locals.security.userId, and ensure the
transaction rolls back on any error so both steps are atomic; locate logic
around the successes/grouped variables and replace the separate Promise.all
calls with the new transactional helper per grouped project.

---

Nitpick comments:
In `@src/lib/prisma/schema.prisma`:
- Around line 301-313: Add indexes on the foreign key columns in the
ProjectActions model: add @@index([ProjectId]) and @@index([UserId]) to the
ProjectActions model so queries filtering by ProjectId or UserId use indexes;
update the Prisma schema (model ProjectActions) accordingly and run prisma
migrate to generate/apply the migration.

In `@src/lib/projects/sse.ts`:
- Around line 325-377: project.ProjectActions is being filtered and mapped three
times and the three DatabaseReads.findMany calls inside actionParams are
executed sequentially; refactor by precomputing three Sets of unique ExternalId
values (e.g., authorIds, ownerGroupIds, productDefIds) from
project.ProjectActions once, then call DatabaseReads.users.findMany,
DatabaseReads.groups.findMany, and DatabaseReads.productDefinitions.findMany in
parallel using Promise.all and the precomputed Sets in the respective where.in
clauses so actionParams is built from the parallel results.

In `@src/lib/server/database/ProjectActions.ts`:
- Around line 15-17: The catch blocks in ProjectActions.ts (in the create and
createMany functions) silently swallow exceptions and return false; change those
catches to accept the error (catch (err)) and log the error before returning so
failures are visible; use the module's established logger (or console.error if
none exists) to emit a contextual message including the function name (e.g.,
"ProjectActions.create" / "ProjectActions.createMany") and the caught error,
then return false as before.

In `@src/routes/`(authenticated)/projects/[id=idNumber]/+page.server.ts:
- Around line 214-226: Move the BuildEnginePublisherId lookup out of the
DatabaseWrites.projectActions.create call so it is fetched before queueing the
job (e.g. alongside the earlier repository/store validation) using
DatabaseReads.stores.findFirst to assign a local variable
(BuildEnginePublisherId) and pass that variable into
DatabaseWrites.projectActions.create; alternatively, if you prefer minimal
change, perform the findFirst just before calling
DatabaseWrites.projectActions.create and provide a sensible fallback (empty
string or null) for Value when BuildEnginePublisherId is undefined.

In `@src/routes/`(authenticated)/projects/[id=idNumber]/+page.svelte:
- Around line 252-256: The code repeatedly filters
projectData.project.ProjectActions inside the product/card rendering, causing
redundant recomputation; compute a single derived array once (e.g., const
filteredProjectActions = projectData.project.ProjectActions.filter(pa =>
pa.ActionType === ProjectActionType.Access || pa.ActionType ===
ProjectActionType.Archival)) outside the product loop or card component and pass
that filteredProjectActions into each card via the projectActions prop instead
of calling .filter(...) per product.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bab7056-3efd-402b-bdb0-60e35575e6a4

📥 Commits

Reviewing files that changed from the base of the PR and between e8d2ee3 and 4065849.

📒 Files selected for processing (28)
  • src/lib/icons/index.ts
  • src/lib/locales/en-US.json
  • src/lib/locales/es-419.json
  • src/lib/locales/fr-FR.json
  • src/lib/prisma.ts
  • src/lib/prisma/migrations/35_project_actions/migration.sql
  • src/lib/prisma/schema.prisma
  • src/lib/products/components/ProductDetails.svelte
  • src/lib/projects/components/ProjectActionEntry.svelte
  • src/lib/projects/components/ProjectActionMenu.svelte
  • src/lib/projects/components/ProjectDetails.svelte
  • src/lib/projects/server.ts
  • src/lib/projects/sse.ts
  • src/lib/server/bullmq/BullWorker.ts
  • src/lib/server/database/Products.ts
  • src/lib/server/database/ProjectActions.ts
  • src/lib/server/database/index.ts
  • src/lib/server/job-executors/build.ts
  • src/lib/server/job-executors/system.ts
  • src/lib/server/job-executors/userTasks.ts
  • src/routes/(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte
  • src/routes/(authenticated)/projects/[id=idNumber]/ProductCard.svelte
  • src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.svelte
  • src/routes/(authenticated)/workflow-instances/[product_id]/+page.server.ts
  • src/routes/(unauthenticated)/api/projects/[id=idNumber]/token/+server.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/server/job-executors/system.ts`:
- Around line 703-752: The code currently only reads a single product
(Products.take:1 ordered by DateCreated) and maps ProductTransitions from
p.Products[0], which can miss transitions and then the subsequent
tx.productTransitions.deleteMany deletes all matching transitions for the
project; change DatabaseReads.projects.findMany so Products is filtered by the
same transitionFilter (Products: { where: transitionFilter, select: {
ProductTransitions: { where: transitionFilter, ... } } }), remove the
orderBy/take:1 so you fetch all products with matching transitions, then in the
prismaInternal.$transaction use p.Products.flatMap(p => p.ProductTransitions) to
build the tx.projectActions.createMany payload so you migrate every matching
ProductTransition before calling tx.productTransitions.deleteMany with the same
transitionFilter and Product: { ProjectId: p.Id } to ensure only migrated rows
are deleted (referencing DatabaseReads.projects.findMany, transitionFilter,
p.Products, prismaInternal.$transaction, tx.projectActions.createMany,
tx.productTransitions.deleteMany).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68fc4fcf-2abb-44a1-ab91-35111d9bc2b0

📥 Commits

Reviewing files that changed from the base of the PR and between 4065849 and 81ee748.

📒 Files selected for processing (5)
  • src/lib/prisma/migrations/35_project_actions/migration.sql
  • src/lib/prisma/schema.prisma
  • src/lib/projects/components/ProjectActionEntry.svelte
  • src/lib/server/job-executors/system.ts
  • src/routes/(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts
✅ Files skipped from review due to trivial changes (2)
  • src/lib/prisma/migrations/35_project_actions/migration.sql
  • src/lib/prisma/schema.prisma
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/routes/(authenticated)/organizations/[id=idNumber]/settings/stores/transfer/+page.server.ts
  • src/lib/projects/components/ProjectActionEntry.svelte

@chrisvire
Copy link
Member

chrisvire commented Mar 26, 2026

How is this sorted? I did these series of actions:

  1. set the description
  2. change ownership to Aidan
  3. change ownership back to me
image

It looks like the migration of the product transitions came in-between two of my actions, but the dates don't line up in the display.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/projects/sse.ts (1)

328-380: Consider parallelizing independent database queries.

The three queries for users, groups, and prodDefs are independent and could be executed concurrently using Promise.all() to reduce overall latency.

♻️ Proposed refactor to parallelize queries
         actionParams: {
-          users: await DatabaseReads.users.findMany({
+          ...(await (async () => {
+            const userIds = project.ProjectActions.filter(
+              (pa) =>
+                pa.ExternalId &&
+                (pa.ActionType === ProjectActionType.Author ||
+                  (pa.ActionType === ProjectActionType.OwnerGroup &&
+                    pa.Action !== ProjectActionString.AssignGroup))
+            ).map((pa) => pa.ExternalId!);
+
+            const groupIds = project.ProjectActions.filter(
+              (pa) =>
+                pa.ExternalId &&
+                pa.ActionType === ProjectActionType.OwnerGroup &&
+                pa.Action === ProjectActionString.AssignGroup
+            ).map((pa) => pa.ExternalId!);
+
+            const prodDefIds = project.ProjectActions.filter(
+              (pa) => pa.ExternalId && pa.ActionType === ProjectActionType.Product
+            ).map((pa) => pa.ExternalId!);
+
+            const [users, groups, prodDefs] = await Promise.all([
+              DatabaseReads.users.findMany({
+                where: { Id: { in: userIds } },
+                select: { Id: true, Name: true }
+              }),
+              DatabaseReads.groups.findMany({
+                where: { Id: { in: groupIds } },
+                select: { Id: true, Name: true }
+              }),
+              DatabaseReads.productDefinitions.findMany({
+                where: { Id: { in: prodDefIds } },
+                select: {
+                  Id: true,
+                  Name: true,
+                  Workflow: { select: { ProductType: true } }
+                }
+              })
+            ]);
+
+            return { users, groups, prodDefs };
+          })())
-            where: {
-              Id: {
-                in: project.ProjectActions.filter(
-                  (pa) =>
-                    pa.ExternalId &&
-                    (pa.ActionType === ProjectActionType.Author ||
-                      (pa.ActionType === ProjectActionType.OwnerGroup &&
-                        pa.Action !== ProjectActionString.AssignGroup))
-                ).map((pa) => pa.ExternalId!)
-              }
-            },
-            select: {
-              Id: true,
-              Name: true
-            }
-          }),
-          groups: await DatabaseReads.groups.findMany({
-            where: {
-              Id: {
-                in: project.ProjectActions.filter(
-                  (pa) =>
-                    pa.ExternalId &&
-                    pa.ActionType === ProjectActionType.OwnerGroup &&
-                    pa.Action === ProjectActionString.AssignGroup
-                ).map((pa) => pa.ExternalId!)
-              }
-            },
-            select: {
-              Id: true,
-              Name: true
-            }
-          }),
-          prodDefs: await DatabaseReads.productDefinitions.findMany({
-            where: {
-              Id: {
-                in: project.ProjectActions.filter(
-                  (pa) => pa.ExternalId && pa.ActionType === ProjectActionType.Product
-                ).map((pa) => pa.ExternalId!)
-              }
-            },
-            select: {
-              Id: true,
-              Name: true,
-              Workflow: {
-                select: {
-                  ProductType: true
-                }
-              }
-            }
-          })
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/projects/sse.ts` around lines 328 - 380, The three independent DB
calls that populate actionParams.users, actionParams.groups and
actionParams.prodDefs should be executed in parallel to reduce latency: create
the three DatabaseReads.findMany promises (the users, groups and
productDefinitions queries referencing project.ProjectActions and
ProjectActionType/ProductActionString) and run them with Promise.all(), then
assign the resolved results back into actionParams.users, actionParams.groups
and actionParams.prodDefs; ensure the select shapes remain the same and preserve
the filtering logic when extracting ExternalId from project.ProjectActions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/projects/sse.ts`:
- Around line 328-380: The three independent DB calls that populate
actionParams.users, actionParams.groups and actionParams.prodDefs should be
executed in parallel to reduce latency: create the three DatabaseReads.findMany
promises (the users, groups and productDefinitions queries referencing
project.ProjectActions and ProjectActionType/ProductActionString) and run them
with Promise.all(), then assign the resolved results back into
actionParams.users, actionParams.groups and actionParams.prodDefs; ensure the
select shapes remain the same and preserve the filtering logic when extracting
ExternalId from project.ProjectActions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8db3e460-7642-4b3a-8f56-deaed2db7dec

📥 Commits

Reviewing files that changed from the base of the PR and between 81ee748 and f680b65.

📒 Files selected for processing (1)
  • src/lib/projects/sse.ts

@FyreByrd
Copy link
Contributor Author

How is this sorted? I did these series of actions:

This has been fixed. I forgot to order by date in the db query.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DB Migration This PR makes changes to the database schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants