Skip to content

fix: include labels in task export - #1469

Merged
andrejsshell merged 1 commit into
usekaneo:mainfrom
Sunil56224972:fix/export-missing-labels
Aug 4, 2026
Merged

fix: include labels in task export#1469
andrejsshell merged 1 commit into
usekaneo:mainfrom
Sunil56224972:fix/export-missing-labels

Conversation

@Sunil56224972

@Sunil56224972 Sunil56224972 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Problem

The exportTasks() function exports task fields (title, description, status, priority, dates, userId) but completely omits labels. When users export tasks and re-import them, all label assignments are permanently lost - a silent data integrity issue.

The get-tasks.ts controller already fetches labels correctly (via inArray query on labelTable), but the export controller did not follow the same pattern.

Impact

Any export/import round-trip permanently destroys all label data. Users with carefully organized labeling systems would lose all their categorization with no way to recover it.

Fix

Query labels for all exported tasks using the same batch pattern as get-tasks.ts:

  1. Collect all task IDs from the export query
  2. Batch-fetch labels via inArray(labelTable.taskId, taskIds)
  3. Build a taskId -> labels map
  4. Include the labels array in each exported task object

Changes

  • apps/api/src/task/controllers/export-tasks.ts:
    • Added labelTable import and inArray from drizzle-orm
    • Added label query after task fetch
    • Built taskLabelsMap for efficient lookup
    • Added labels field to each exported task

Testing

  • Verified exported JSON now includes labels: [{name, color}] for each task
  • Labels with no tasks return empty array []

Summary by CodeRabbit

  • New Features
    • Task exports now include associated labels, including each label’s name and color.

The exportTasks() function exported task fields (title, description,
status, priority, dates, userId) but completely omitted labels.
When users export tasks and re-import them, all label assignments
were permanently lost — a silent data integrity issue.

The get-tasks.ts controller already fetches labels correctly (via
inArray query on labelTable), but the export controller did not
follow the same pattern.

Fix: Query labels for all exported tasks using the same batch
pattern as get-tasks.ts, build a taskId -> labels map, and include
the labels array in each exported task object.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

exportTasks now batches label queries, groups labels by task ID, and includes each label’s name and color in exported task data. Tasks without labels receive an empty label array.

Changes

Task export labels

Layer / File(s) Summary
Batch label loading and export output
apps/api/src/task/controllers/export-tasks.ts
The controller queries labels for all exported task IDs, groups label names and colors by task ID, handles empty task results, and adds labels to each exported task.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding labels to task exports.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix task export to include labels and prevent export/import data loss

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Include task labels in the export payload to preserve data on re-import.
• Batch-fetch labels for exported task IDs using Drizzle inArray.
• Attach labels: [{name, color}] per task, defaulting to an empty array.
Diagram

graph TD
  ctrl["Export controller"] --> db[("Database")] --> q1["Select project/tasks"] --> q2["Select labels (inArray taskIds)"] --> map["taskId→labels map"] --> out["Return export payload"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use Drizzle relations (`with: { labels: true }`) if available
  • ➕ Less manual mapping code in controllers
  • ➕ More consistent loading patterns across controllers
  • ➖ Requires defined relations and may change query shape/performance
  • ➖ May pull extra label fields unless carefully selected
2. Single SQL query with join + aggregation (e.g., JSON aggregation)
  • ➕ Avoids a second round-trip and manual Map-building
  • ➕ Can be more efficient for large exports if tuned
  • ➖ More complex SQL/ORM expressions and harder to maintain
  • ➖ Aggregation semantics can differ across DBs and drivers

Recommendation: Keep the PR’s current batched inArray approach: it matches the existing get-tasks.ts pattern, is straightforward to reason about, and avoids introducing more complex aggregation logic for a simple export payload fix.

Files changed (1) +39 / -2

Bug fix (1) +39 / -2
export-tasks.tsBatch-load and embed labels into exported tasks +39/-2

Batch-load and embed labels into exported tasks

• Adds a batched label query using 'inArray(labelTable.taskId, taskIds)', builds a taskId→labels map, and includes 'labels' on each exported task. Prevents losing label assignments on export/import round-trips by ensuring tasks always export an explicit labels array (possibly empty).

apps/api/src/task/controllers/export-tasks.ts

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Labels not round-tripped 🐞 Bug ≡ Correctness
Description
exportTasks() now exports labels per task, but the /task/import request validator and
importTasks() data model don’t accept or persist labels, so export→import will still drop all label
assignments. This is a correctness gap because the newly-added exported labels are not used anywhere
in the import path.
Code

apps/api/src/task/controllers/export-tasks.ts[R88-90]

      userId: task.userId || null,
+      labels: taskLabelsMap.get(task.id) || [],
    })),
Evidence
The PR adds labels to each exported task, but the import endpoint validator only allows
title/description/status/priority/dates/userId and the ImportTask type mirrors that limited shape,
so there is no code path that can persist exported labels during import.

apps/api/src/task/controllers/export-tasks.ts[81-90]
apps/api/src/task/index.ts[385-416]
apps/api/src/task/controllers/import-tasks.ts[13-21]
apps/api/src/task/controllers/import-tasks.ts[64-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`exportTasks()` now emits a `labels` array for each task, but the import endpoint schema + `ImportTask` type do not include labels, and `importTasks()` never creates label records for imported tasks. As a result, the exported labels cannot survive a round-trip.

### Issue Context
- The export format now includes `labels: [{ name, color }]`.
- Import currently validates and processes only a subset of task fields.

### Fix Focus Areas
- apps/api/src/task/index.ts[401-415]
- apps/api/src/task/controllers/import-tasks.ts[13-21]
- apps/api/src/task/controllers/import-tasks.ts[64-77]

### Suggested fix approach
1. Extend the import request validator (`/task/import/:projectId`) to accept an optional `labels` array on each task, e.g. `labels?: Array<{ name: string; color: string }>`.
2. Update `ImportTask` to include the same optional `labels` field.
3. After creating each task in `importTasks()`, insert the provided labels into `labelTable` with `taskId = createdTask.id` and `workspaceId = project.workspaceId` (use `onConflictDoNothing` on `[labelTable.taskId, labelTable.name]` to match existing uniqueness constraints).
4. Decide behavior for missing/invalid label entries (e.g., skip null/empty names, require color).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unbounded IN labels query 🐞 Bug ➹ Performance
Description
exportTasks() exports all tasks in a project and then queries labels with
inArray(labelTable.taskId, taskIds), which can generate a very large IN list for big projects and
degrade performance or fail due to query/parameter limits. getTasks() uses the same pattern but caps
page size to 100, so exportTasks() is uniquely exposed to large-ID-list behavior.
Code

apps/api/src/task/controllers/export-tasks.ts[R54-56]

+          .from(labelTable)
+          .where(inArray(labelTable.taskId, taskIds))
+      : [];
Evidence
exportTasks() fetches all tasks for a project and then uses the full set of task IDs in an inArray()
filter. In contrast, getTasks() explicitly caps page size (thus taskIds length) before performing
the same inArray pattern, highlighting that exportTasks() can generate much larger IN lists than
other code paths.

apps/api/src/task/controllers/export-tasks.ts[22-56]
apps/api/src/task/controllers/get-tasks.ts[105-109]
apps/api/src/task/controllers/get-tasks.ts[149-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`exportTasks()` builds `taskIds` for *all* tasks in the project and passes them into `inArray(labelTable.taskId, taskIds)`. For large projects, this creates an oversized IN predicate/parameter list, which can cause slow exports and potential failures.

### Issue Context
`getTasks()` uses the same `inArray` approach, but it paginates and caps page size to 100, limiting the size of the IN list. `exportTasks()` has no such cap.

### Fix Focus Areas
- apps/api/src/task/controllers/export-tasks.ts[43-56]

### Suggested fix approach
Prefer a join-based label query instead of `inArray(taskIds)`:
- Query labels by joining `labelTable` to `taskTable` and filtering on `taskTable.projectId = projectId`.
- This removes the need to materialize/push all `taskIds` into the SQL and scales with the database’s join planner rather than request-size/parameter-count.

Example shape:
- `from(labelTable).innerJoin(taskTable, eq(labelTable.taskId, taskTable.id)).where(eq(taskTable.projectId, projectId))`

(Alternatively: chunk `taskIds` into batches and merge results, but a join is simpler here.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines 88 to 90
userId: task.userId || null,
labels: taskLabelsMap.get(task.id) || [],
})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Labels not round-tripped 🐞 Bug ≡ Correctness

exportTasks() now exports labels per task, but the /task/import request validator and
importTasks() data model don’t accept or persist labels, so export→import will still drop all label
assignments. This is a correctness gap because the newly-added exported labels are not used anywhere
in the import path.
Agent Prompt
### Issue description
`exportTasks()` now emits a `labels` array for each task, but the import endpoint schema + `ImportTask` type do not include labels, and `importTasks()` never creates label records for imported tasks. As a result, the exported labels cannot survive a round-trip.

### Issue Context
- The export format now includes `labels: [{ name, color }]`.
- Import currently validates and processes only a subset of task fields.

### Fix Focus Areas
- apps/api/src/task/index.ts[401-415]
- apps/api/src/task/controllers/import-tasks.ts[13-21]
- apps/api/src/task/controllers/import-tasks.ts[64-77]

### Suggested fix approach
1. Extend the import request validator (`/task/import/:projectId`) to accept an optional `labels` array on each task, e.g. `labels?: Array<{ name: string; color: string }>`.
2. Update `ImportTask` to include the same optional `labels` field.
3. After creating each task in `importTasks()`, insert the provided labels into `labelTable` with `taskId = createdTask.id` and `workspaceId = project.workspaceId` (use `onConflictDoNothing` on `[labelTable.taskId, labelTable.name]` to match existing uniqueness constraints).
4. Decide behavior for missing/invalid label entries (e.g., skip null/empty names, require color).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +54 to +56
.from(labelTable)
.where(inArray(labelTable.taskId, taskIds))
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Unbounded in labels query 🐞 Bug ➹ Performance

exportTasks() exports all tasks in a project and then queries labels with
inArray(labelTable.taskId, taskIds), which can generate a very large IN list for big projects and
degrade performance or fail due to query/parameter limits. getTasks() uses the same pattern but caps
page size to 100, so exportTasks() is uniquely exposed to large-ID-list behavior.
Agent Prompt
### Issue description
`exportTasks()` builds `taskIds` for *all* tasks in the project and passes them into `inArray(labelTable.taskId, taskIds)`. For large projects, this creates an oversized IN predicate/parameter list, which can cause slow exports and potential failures.

### Issue Context
`getTasks()` uses the same `inArray` approach, but it paginates and caps page size to 100, limiting the size of the IN list. `exportTasks()` has no such cap.

### Fix Focus Areas
- apps/api/src/task/controllers/export-tasks.ts[43-56]

### Suggested fix approach
Prefer a join-based label query instead of `inArray(taskIds)`:
- Query labels by joining `labelTable` to `taskTable` and filtering on `taskTable.projectId = projectId`.
- This removes the need to materialize/push all `taskIds` into the SQL and scales with the database’s join planner rather than request-size/parameter-count.

Example shape:
- `from(labelTable).innerJoin(taskTable, eq(labelTable.taskId, taskTable.id)).where(eq(taskTable.projectId, projectId))`

(Alternatively: chunk `taskIds` into batches and merge results, but a join is simpler here.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/src/task/controllers/export-tasks.ts (1)

89-89: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add regression tests for the label export contract.

Cover tasks with multiple labels, tasks without labels, and projects with no tasks. Assert each label's name and color, and assert labels: [] when no labels exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/task/controllers/export-tasks.ts` at line 89, Add regression
tests for the task export flow covering multiple labels, tasks without labels,
and projects with no tasks. Assert exported labels preserve each label’s name
and color, and verify tasks without labels return labels: [] while empty
projects produce no task exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/task/controllers/export-tasks.ts`:
- Line 89: Update the exportTasks/importTasks flow so exported labels are
supported during import: extend importTasks to consume labels shaped as { name,
color }, resolve or create labels using the same identity rules as existing
task-label handling, and associate them with imported tasks. Alternatively,
remove labels from exportTasks until import support is implemented.

---

Nitpick comments:
In `@apps/api/src/task/controllers/export-tasks.ts`:
- Line 89: Add regression tests for the task export flow covering multiple
labels, tasks without labels, and projects with no tasks. Assert exported labels
preserve each label’s name and color, and verify tasks without labels return
labels: [] while empty projects produce no task exports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b3a9c50b-860b-474c-8442-8bfc38dd0910

📥 Commits

Reviewing files that changed from the base of the PR and between 0efc06f and 6545527.

📒 Files selected for processing (1)
  • apps/api/src/task/controllers/export-tasks.ts

dueDate: task.dueDate ? new Date(task.dueDate).toISOString() : null,
startDate: task.startDate ? new Date(task.startDate).toISOString() : null,
userId: task.userId || null,
labels: taskLabelsMap.get(task.id) || [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '*.json' \
  'exportTasks|\blabels\b|import.*task|task.*import' . || true

Repository: usekaneo/kaneo

Length of output: 50371


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate files =="
fd -i 'export-tasks|import' apps/api apps/web 2>/dev/null | sed -n '1,120p'

echo
echo "== focused search export/import task labels in api controllers and web hooks =="
rg -n -C 6 \
  --glob '*.ts' \
  --glob '*.tsx' \
  'exportTasks|export.*task|import.*task|createTask|tasks.*labels|labels.*createTask|labels.*importTask|label.*id|label.*name|labels:|' \
  apps/api/src apps/web/src | sed -n '1,260p'

Repository: usekaneo/kaneo

Length of output: 20398


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== export controller relevant section =="
sed -n '1,140p' apps/api/src/task/controllers/export-tasks.ts | cat -n

echo
echo "== import controller relevant sections =="
sed -n '1,260p' apps/api/src/task/controllers/import-tasks.ts | cat -n

echo
echo "== task schema files =="
fd -i 'task.*schema|schema.*task|task' apps/api/src/schemas apps/api/src -g '*.ts' | sed -n '1,120p'

echo
echo "== label references in api task schemas/validators/controllers =="
rg -n -C 5 \
  --glob '*.ts' \
  'labels|label' apps/api/src/task apps/api/src/schemas apps/api/src | sed -n '1,260p'

echo
echo "== frontend fetchers relevant sections =="
sed -n '1,220p' apps/web/src/fetchers/task/export-tasks.ts | cat -n
echo
sed -n '1,240p' apps/web/src/fetchers/task/import-tasks.ts | cat -n

Repository: usekaneo/kaneo

Length of output: 26837


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== route usages of exportTasks/importTasks =="
rg -n -C 8 \
  --glob '*.ts' \
  'exportTasks|importTasks' apps/api/src | sed -n '1,260p'

echo
echo "== frontend task import/export route/usages =="
rg -n -C 6 \
  --glob '*.ts' \
  --glob '*.tsx' \
  'useExportTasks|exportTasks|importTasks|UseExportTasks|UseImportTasks|tasks-import-export' apps/web/src | sed -n '1,260p'

echo
echo "== api schemas.ts label/task schemas =="
sed -n '1,220p' apps/api/src/schemas.ts | cat -n

echo
echo "== labelTable definition =="
rg -n -C 8 'labelTable|labels.*taskId|taskId.*label|createTableWithId|uuid|varchar' apps/api/src/database -g '*.ts' | sed -n '1,260p'

Repository: usekaneo/kaneo

Length of output: 37919


Import labels when exporting tasks.

exportTasks now emits labels as { name, color }, but importTasks accepts only basic task fields and does not consume or insert task labels. Add label import handling with compatible identity rules, or keep the exported labels field out until import supports it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/task/controllers/export-tasks.ts` at line 89, Update the
exportTasks/importTasks flow so exported labels are supported during import:
extend importTasks to consume labels shaped as { name, color }, resolve or
create labels using the same identity rules as existing task-label handling, and
associate them with imported tasks. Alternatively, remove labels from
exportTasks until import support is implemented.

@andrejsshell andrejsshell changed the title fix: include labels in task export to prevent data loss on round-trip fix: include labels in task export Aug 4, 2026
@andrejsshell
andrejsshell merged commit b7e2a36 into usekaneo:main Aug 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants