Skip to content

Add queued markdown imports#365

Merged
taqh merged 10 commits into
mainfrom
feat/add-data-imports
Jun 27, 2026
Merged

Add queued markdown imports#365
taqh merged 10 commits into
mainfrom
feat/add-data-imports

Conversation

@taqh

@taqh taqh commented Jun 27, 2026

Copy link
Copy Markdown
Member

Description

Adds file-based data import and export flows for Marble workspaces, centered on queued Markdown imports. The CMS settings page can now upload Markdown files or ZIP archives, create import jobs, and show recent import/export activity while the jobs worker parses supported files and creates posts asynchronously.

Motivation and Context

Workspace data imports need to handle larger uploads without blocking the CMS request lifecycle. Moving Markdown processing into queued jobs gives users a clearer import flow, lets ZIP archives create one post per supported file, and keeps import/export status visible from settings.

How to Test

  1. Run the migration and generate Prisma clients:
    cd packages/db
    pnpm exec prisma migrate dev --name add_imports_and_exports
    pnpm exec prisma generate
  2. Start local Workers from the repo root:
    pnpm workers:dev
  3. Start the CMS with MARBLE_API_URL=http://localhost:8787 and matching SYSTEM_SECRET.
  4. Open workspace settings and go to the Data page.
  5. Upload a Markdown file and confirm an import job is created.
  6. Confirm the imported Markdown file becomes a post with the expected title, slug, content, description, tags, category, and author metadata.
  7. Upload a ZIP archive containing multiple Markdown files and confirm each supported Markdown file becomes a post.
  8. Upload a ZIP archive that also contains unsupported files and confirm unsupported entries are skipped instead of blocking the import.

Screenshots (if applicable)

N/A

Video Demo (if applicable)

N/A

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

  • New Features
    • Added a Data settings page with Import and Export controls, including workspace sidebar navigation.
    • Updated the import modal to start a backend/worker import (file upload or URL input) and display import job progress/history.
  • Bug Fixes
    • Standardized empty-state/tooltips and buttons to consistently use Import.
    • Improved HTML sanitization for imported/edited content (including stricter handling of unsafe <input> elements).
    • URL-based imports are now explicitly rejected as unsupported; import jobs use the updated processing workflow.

@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marble-app Ready Ready Preview, Comment Jun 27, 2026 9:57pm
marble-web Ready Ready Preview, Comment Jun 27, 2026 9:57pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aaba7f79-9290-4d39-8d26-83193c83d31b

📥 Commits

Reviewing files that changed from the base of the PR and between 655bdbc and 603f840.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/cms/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cms/package.json

Walkthrough

Replaces the client-side import flow with a backend import job pipeline, adds a new Data settings page and import history UI, and rewires CMS/API code to shared sanitize and queue modules. It also adds worker parsing, cleanup jobs, and the new import.process task flow.

Changes

Backend-driven import pipeline

Layer / File(s) Summary
Shared package contracts and rewiring
packages/utils/package.json, packages/utils/src/functions/sanitize.ts, packages/parser/package.json, packages/events/src/queue.ts, .gitignore, apps/api/package.json, apps/api/src/lib/fields.ts, apps/api/src/routes/posts.ts, apps/api/src/validations/misc.ts, apps/cms/src/app/api/*, apps/jobs/package.json, apps/jobs/src/lib/export.ts
Moves sanitizers into @marble/utils/sanitize, updates queue/task contracts to import.process, removes the local API sanitizer module, and rewires CMS/API import sites to the shared modules.
Import validation and CMS request helpers
apps/cms/src/lib/validations/import.ts, apps/cms/src/utils/import.ts, apps/cms/src/lib/queries/keys.ts
Adds request-source validation, import job serialization, format detection, request parsing, and the workspace import query key.
/api/data/import route
apps/cms/src/app/api/data/import/route.ts, apps/api/src/validations/misc.ts
Implements import job listing and creation, file upload to R2, job persistence, queue enqueueing, and task schema literal changes for the new import message type.
Import worker, content parsing, and task dispatch
apps/jobs/src/types/import.ts, apps/jobs/src/utils/import-content.ts, apps/jobs/src/utils/import-records.ts, apps/jobs/src/lib/import.ts, apps/jobs/src/consumers/tasks.ts, apps/jobs/src/consumers/dlq.ts, apps/jobs/src/lib/constants.ts
Adds Markdown/ZIP import parsing, import record helpers, end-to-end worker processing, and the consumer/DLQ wiring for import.process.
Stale import cleanup
apps/jobs/src/scheduled/imports.ts, apps/jobs/src/scheduled/cleanup.ts
Adds scheduled cleanup for stale active import jobs and old terminal jobs, including upload deletion and batch integration.
CMS import UI and data settings
apps/cms/src/components/posts/import-modal.tsx, apps/cms/src/components/settings/fields/import.tsx, apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/data/page-client.tsx, apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/data/page.tsx, apps/cms/src/components/shared/dropzone.tsx, apps/cms/src/components/nav/nav-settings.tsx, apps/cms/src/components/layout/page-header.tsx, apps/cms/src/components/settings/fields/export.tsx, apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx, apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx, apps/cms/src/components/posts/data-view.tsx
Replaces the import modal with a backend-driven file/URL flow, adds the import settings list and Data settings page, updates navigation/header labels, and changes the dropzone and import/export UI text and endpoints.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ImportModal as ImportModal
  participant ImportRoute as /api/data/import
  participant R2 as R2 Storage
  participant DB
  participant TaskQueue
  participant Worker as runImport

  User->>ImportModal: choose file or URL
  ImportModal->>ImportRoute: POST upload request
  ImportRoute->>R2: store upload
  ImportRoute->>DB: create import job
  ImportRoute->>TaskQueue: enqueue import.process
  TaskQueue->>Worker: deliver job
  Worker->>R2: read upload
  Worker->>DB: write import records and final status
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • usemarble/marble#218: Introduces the earlier markdown import endpoint and UI that this PR replaces with /api/data/import and the background job pipeline.
  • usemarble/marble#293: Touches the CMS import modal area that this PR rewires into the backend-driven import flow.
  • usemarble/marble#361: Changes the same background task and export/import queue surface, including the import.process contract.

Suggested reviewers

  • mezotv
  • Hiccup-za

🐇 I hopped through uploads, quick and bright,
New import jobs now start at night.
Data tabs and queues align,
Markdown, zip, and posts now shine.
Thump thump—fresh pathways feel just right ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.44% 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
Title check ✅ Passed The title is concise and clearly points to the main change: queued markdown imports.
Description check ✅ Passed The description follows the template and includes the required sections with sufficient detail.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-data-imports

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-jobs 655bdbc Commit Preview URL

Branch Preview URL
Jun 27 2026, 09:42 PM

@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: 12

Caution

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

⚠️ Outside diff range comments (1)
apps/jobs/src/lib/import.ts (1)

116-132: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Atomically claim queued import jobs before processing.

The current entrypoint can process jobs already marked processing, and the queued → processing transition is not conditional. Duplicate queue deliveries can therefore run Lines 194-225 twice for the same job and create duplicate posts/import items.

Proposed claim guard
-  if (job.status !== "queued" && job.status !== "processing") {
+  if (job.status !== "queued") {
     return;
   }
 
-  if (job.status === "queued") {
-    await db.importJob.update({
-      where: { id: job.id },
-      data: { status: "processing", startedAt: job.startedAt ?? new Date() },
-    });
+  const claim = await db.importJob.updateMany({
+    where: { id: job.id, status: "queued" },
+    data: { status: "processing", startedAt: job.startedAt ?? new Date() },
+  });
+
+  if (claim.count === 0) {
+    return;
   }
🤖 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/jobs/src/lib/import.ts` around lines 116 - 132, Atomically claim import
jobs in runImport before any work starts, instead of processing existing
processing jobs directly. Update the queued-to-processing transition in
runImport so it only succeeds when the job is still queued (use a conditional
update/claim pattern on db.importJob.update or a helper around getImportJob),
and only continue into the import flow after that claim succeeds. Make the job
status check in runImport refuse already-processing jobs unless they were just
claimed by this invocation, so duplicate deliveries cannot execute the later
import/post creation logic twice.
🧹 Nitpick comments (5)
apps/cms/src/app/api/data/import/route.ts (1)

99-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid console.error in this route.

Please route this through the project's normal logging utility instead. As per coding guidelines, **/*.{ts,tsx,js,jsx}: Don't use console.

🤖 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/cms/src/app/api/data/import/route.ts` at line 99, The import route is
logging with console.error, which violates the project’s no-console guideline.
Update the error handling in the route handler for the import upload path to use
the project’s standard logging utility instead of console.error, preserving the
same error context and message while routing it through the existing logger used
elsewhere in the app.

Source: Coding guidelines

apps/jobs/src/scheduled/imports.ts (1)

28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace console.* in the new cleanup task.

Use the project logging abstraction or add an explicit lint exception if worker cleanup intentionally logs directly. As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Don't use console."

Also applies to: 117-123

🤖 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/jobs/src/scheduled/imports.ts` at line 28, The new cleanup task is
logging directly with console.error, which violates the project logging
guideline. Update the cleanup flow in imports.ts to use the existing logging
abstraction used by the worker/job code, or add an explicit lint exception only
if direct console logging is truly intentional. Make the change in the cleanup
task logic around the import deletion path so the log still includes the import
id and error details without using console.*.

Source: Coding guidelines

apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/data/page.tsx (1)

9-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unnecessary async modifier.

Page does not await anything, so this adds an avoidable promise boundary and violates the repo rule for async functions. As per coding guidelines, "Make sure async functions actually use await".

Suggested fix
-async function Page() {
+function Page() {
   return <PageClient />;
 }
🤖 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/cms/src/app/`(main)/[workspace]/(dashboard)/settings/data/page.tsx
around lines 9 - 11, The Page component is marked async even though it does not
await anything, so remove the unnecessary async modifier from Page and keep it
as a normal component that returns PageClient directly. Use the Page function in
the settings data page as the target for this cleanup to comply with the repo
rule that async functions must actually use await.

Source: Coding guidelines

apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx (1)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the repo-standard icon package here.

This change introduces Hugeicons for the import action, but the repository guidelines require Phosphor icons for UI icons. Please switch this trigger to a Phosphor icon instead. As per coding guidelines, "Always use the Phosphor Icons package for icons".

Also applies to: 133-142

🤖 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/cms/src/app/`(main)/[workspace]/(dashboard)/posts/page-client.tsx around
lines 3 - 4, The import/action trigger in the posts page is using Hugeicons
instead of the repo-standard Phosphor icons. Update the icon imports in
page-client.tsx and the related icon usage in the import action
component/trigger (including the code around the referenced import action block)
to use a Phosphor icon component instead, and remove the Hugeicons-specific
imports and JSX so the UI follows the repository icon guideline.

Source: Coding guidelines

apps/cms/src/components/settings/fields/import.tsx (1)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the repo-standard icon package here.

This new settings card uses Hugeicons, but the repository guidelines require Phosphor icons for UI iconography. Please swap this to a Phosphor equivalent before merge. As per coding guidelines, "Always use the Phosphor Icons package for icons".

Also applies to: 115-119

🤖 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/cms/src/components/settings/fields/import.tsx` around lines 3 - 5, The
settings field icon import is using Hugeicons instead of the repository-standard
Phosphor package. Update the icon usage in the import settings component by
replacing FileImportIcon and HugeiconsIcon with the appropriate Phosphor icon
components, and adjust the JSX in the related settings card to use the new icon
API consistently. Keep the Button usage unchanged and ensure all iconography in
this component follows the same Phosphor pattern.

Source: Coding guidelines

🤖 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/cms/src/app/api/data/import/route.ts`:
- Around line 88-123: The import route currently uploads to R2 before creating
the import job, so a failure in db.importJob.create() can leave an orphaned
imports/... object behind. Update the handler in route.ts to track the uploaded
object key and wrap the db.importJob.create() call in error handling that
deletes the R2 object when job creation fails; keep the cleanup tied to the
existing uploadKey/jobData flow so the file upload and database record stay
consistent.
- Around line 111-115: The import route is still enqueuing URL-based jobs via
the `jobData` branch that sets `source: "url"` and `sourceUrl`, but the current
worker contract only supports file/R2 imports. Update the `route.ts` import
handler to reject URL imports for now by returning an error response instead of
creating that `jobData` shape, and keep the existing file-backed path intact for
the worker to consume.

In `@apps/cms/src/app/api/posts/route.ts`:
- Around line 11-14: The post creation flow is waiting on dashboard event
delivery, which keeps a slow or half-open internal events request on the
mutation critical path. Update the shared helper in "`@/lib/queues/events`" around
emitDashboardEvent/logDashboardEventError to use a non-blocking delivery
strategy or a bounded timeout, or move the dispatch so the route handler returns
immediately after the transaction commits. Keep the post mutation path in the
route handler independent of the event call so a successful write is not delayed
by dashboard event delivery.

In `@apps/cms/src/components/settings/fields/import.tsx`:
- Around line 80-101: The useQuery in import.tsx currently treats fetch failures
as missing data, so the empty-state UI can mask real outages. Update the imports
view logic around useQuery, latestJobs, and the empty-state rendering to branch
on the query error/loading state instead of relying only on data being
undefined. Add an explicit error state with a retry action for failed requests,
while keeping the existing imports list and polling behavior for successful
responses.
- Around line 18-24: The import status handling in the `status` union and
related polling/spinner checks is missing the backend’s `discovering` state, so
the job can appear idle too early. Update the local status type in the import
component and the active-state logic used by the polling/rendering paths
(including the checks around the import request flow) to treat `discovering` the
same as the other in-progress states. Use the `status` union and the
polling/spinner conditions in this component to locate and adjust all affected
branches.

In `@apps/cms/src/utils/import.ts`:
- Around line 88-124: The import request parser in importRequestSource currently
assumes request.formData() always succeeds, which can turn malformed multipart
or unsupported bodies into a 500. Wrap the request.formData() call in error
handling and return a validation-style error response when parsing fails, before
the existing file/url checks and importRequestSourceSchema validation run.

In `@apps/jobs/src/scheduled/imports.ts`:
- Around line 74-82: The stale-job cleanup in imports.ts can overwrite a job
that finished after selection because the current db.importJob.update call only
filters by id. Update the logic in the scheduled import cleanup flow around the
job status mutation to use a guarded db.importJob.updateMany (or equivalent
conditional update) that повторяет the stale/status predicate from the earlier
findMany selection before setting terminal fields like status, failedAt,
errorMessage, and uploadKey. Then only apply the failure mutation when the
conditional update actually matches the still-stale job.

In `@apps/jobs/src/utils/import-content.ts`:
- Around line 152-155: Update isMdxExportStart() in import-content.ts so it also
recognizes MDX re-export statements, not just declarations and export default.
Extend the regex to catch forms like export { ... } from ... and export * from
..., so stripMdxModuleSyntax() removes these before Markdown parsing. Keep the
change localized to the export-detection helper used by stripMdxModuleSyntax().
- Around line 246-270: The ZIP import logic in parseMarkdownZipImport currently
checks MAX_ZIP_EXTRACTED_BYTES only after unzipSync(bytes) has already
materialized all entries, so oversized archives can inflate memory before the
guard runs. Move the limit enforcement into unzipSync via its filter hook, using
each entry’s originalSize to stop processing once the cumulative extracted size
would exceed the cap, or switch parseMarkdownZipImport to the streaming Unzip
API if needed; keep the existing file-count and markdown-file checks intact.

In `@apps/jobs/src/utils/import-records.ts`:
- Around line 30-73: The slug generation in getUniquePostSlug and
getUniqueAuthorSlug is still vulnerable to races because it checks uniqueness
before the eventual create/upsert in importMarkdownFile and getImportAuthor.
Move slug resolution to the write path or retry on unique-constraint failure so
tx.post.create and db.author.upsert cannot lose to a concurrent writer; keep the
helper names as the central location for this logic and ensure the final
persisted slug is chosen atomically.

In `@packages/events/src/queue.ts`:
- Around line 28-30: The shared queue contract in TaskMessage has been narrowed
too early, and tasks.ts now treats old import.create messages as unknown. Keep
backward-compatible handling for the legacy import.create variant in the task
consumer while the queue drains, or add an explicit migration/purge step before
removing it; make sure TaskMessage and the consumer logic in tasks.ts stay
aligned until no old messages remain.

In `@packages/utils/src/functions/sanitize.ts`:
- Line 68: The input sanitization rule is still stripping the disabled state
from kept checkbox inputs, so imported task-list checkboxes become interactive
after sanitizeHtml() runs. Update the allowed input attributes in sanitize.ts so
the sanitizeHtml/exclusiveFilter path preserves disabled for checkbox inputs
while still allowing type and checked, and make sure any related attribute list
entries referenced by the comment are updated consistently.

---

Outside diff comments:
In `@apps/jobs/src/lib/import.ts`:
- Around line 116-132: Atomically claim import jobs in runImport before any work
starts, instead of processing existing processing jobs directly. Update the
queued-to-processing transition in runImport so it only succeeds when the job is
still queued (use a conditional update/claim pattern on db.importJob.update or a
helper around getImportJob), and only continue into the import flow after that
claim succeeds. Make the job status check in runImport refuse already-processing
jobs unless they were just claimed by this invocation, so duplicate deliveries
cannot execute the later import/post creation logic twice.

---

Nitpick comments:
In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/posts/page-client.tsx:
- Around line 3-4: The import/action trigger in the posts page is using
Hugeicons instead of the repo-standard Phosphor icons. Update the icon imports
in page-client.tsx and the related icon usage in the import action
component/trigger (including the code around the referenced import action block)
to use a Phosphor icon component instead, and remove the Hugeicons-specific
imports and JSX so the UI follows the repository icon guideline.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/settings/data/page.tsx:
- Around line 9-11: The Page component is marked async even though it does not
await anything, so remove the unnecessary async modifier from Page and keep it
as a normal component that returns PageClient directly. Use the Page function in
the settings data page as the target for this cleanup to comply with the repo
rule that async functions must actually use await.

In `@apps/cms/src/app/api/data/import/route.ts`:
- Line 99: The import route is logging with console.error, which violates the
project’s no-console guideline. Update the error handling in the route handler
for the import upload path to use the project’s standard logging utility instead
of console.error, preserving the same error context and message while routing it
through the existing logger used elsewhere in the app.

In `@apps/cms/src/components/settings/fields/import.tsx`:
- Around line 3-5: The settings field icon import is using Hugeicons instead of
the repository-standard Phosphor package. Update the icon usage in the import
settings component by replacing FileImportIcon and HugeiconsIcon with the
appropriate Phosphor icon components, and adjust the JSX in the related settings
card to use the new icon API consistently. Keep the Button usage unchanged and
ensure all iconography in this component follows the same Phosphor pattern.

In `@apps/jobs/src/scheduled/imports.ts`:
- Line 28: The new cleanup task is logging directly with console.error, which
violates the project logging guideline. Update the cleanup flow in imports.ts to
use the existing logging abstraction used by the worker/job code, or add an
explicit lint exception only if direct console logging is truly intentional.
Make the change in the cleanup task logic around the import deletion path so the
log still includes the import id and error details without using console.*.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2d25954-714c-4a4f-84b3-c49779c8f16e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a83e27 and a7493fb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (56)
  • .gitignore
  • apps/api/package.json
  • apps/api/src/lib/fields.ts
  • apps/api/src/lib/sanitize.ts
  • apps/api/src/routes/posts.ts
  • apps/api/src/validations/misc.ts
  • apps/cms/package.json
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/data/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/data/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx
  • apps/cms/src/app/api/authors/[id]/route.ts
  • apps/cms/src/app/api/authors/route.ts
  • apps/cms/src/app/api/categories/[id]/route.ts
  • apps/cms/src/app/api/categories/route.ts
  • apps/cms/src/app/api/data/export/[id]/download/route.ts
  • apps/cms/src/app/api/data/export/route.ts
  • apps/cms/src/app/api/data/import/route.ts
  • apps/cms/src/app/api/media/route.ts
  • apps/cms/src/app/api/posts/[id]/fields/route.ts
  • apps/cms/src/app/api/posts/[id]/route.ts
  • apps/cms/src/app/api/posts/import/route.ts
  • apps/cms/src/app/api/posts/route.ts
  • apps/cms/src/app/api/tags/[id]/route.ts
  • apps/cms/src/app/api/tags/route.ts
  • apps/cms/src/app/api/upload/complete/route.ts
  • apps/cms/src/components/layout/page-header.tsx
  • apps/cms/src/components/nav/nav-settings.tsx
  • apps/cms/src/components/posts/data-view.tsx
  • apps/cms/src/components/posts/import-modal.tsx
  • apps/cms/src/components/settings/fields/export.tsx
  • apps/cms/src/components/settings/fields/import.tsx
  • apps/cms/src/components/shared/dropzone.tsx
  • apps/cms/src/lib/queries/keys.ts
  • apps/cms/src/lib/queues/events.ts
  • apps/cms/src/lib/queues/tasks.ts
  • apps/cms/src/lib/validations/import.ts
  • apps/cms/src/utils/import.ts
  • apps/jobs/package.json
  • apps/jobs/src/consumers/dlq.ts
  • apps/jobs/src/consumers/tasks.ts
  • apps/jobs/src/lib/constants.ts
  • apps/jobs/src/lib/export.ts
  • apps/jobs/src/lib/import.ts
  • apps/jobs/src/scheduled/cleanup.ts
  • apps/jobs/src/scheduled/imports.ts
  • apps/jobs/src/types/env.ts
  • apps/jobs/src/types/import.ts
  • apps/jobs/src/utils/import-content.ts
  • apps/jobs/src/utils/import-records.ts
  • apps/jobs/tsconfig.json
  • apps/jobs/worker-configuration.d.ts
  • packages/events/src/queue.ts
  • packages/parser/package.json
  • packages/utils/package.json
  • packages/utils/src/functions/sanitize.ts
💤 Files with no reviewable changes (7)
  • apps/api/package.json
  • apps/cms/src/app/api/posts/import/route.ts
  • apps/api/src/lib/sanitize.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx
  • apps/jobs/src/consumers/dlq.ts
  • apps/cms/package.json
  • apps/api/src/validations/misc.ts

Comment thread apps/cms/src/app/api/data/import/route.ts Outdated
Comment thread apps/cms/src/app/api/data/import/route.ts Outdated
Comment on lines 11 to +14
import {
emitDashboardEvent,
logDashboardEventError,
} from "@/lib/events/dispatch";
import { getDashboardPosts } from "@/lib/queries/dashboard/posts";
} from "@/lib/queues/events";

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep dashboard event delivery off the mutation critical path.

@/lib/queues/events posts to ${MARBLE_API_URL}/internal/events with fetch() and no timeout, and this handler awaits it after the transaction commits. If the internal API is slow or half-open, post creation can hang even though the write already succeeded; fix this once in the shared helper or dispatch it after the response.

🤖 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/cms/src/app/api/posts/route.ts` around lines 11 - 14, The post creation
flow is waiting on dashboard event delivery, which keeps a slow or half-open
internal events request on the mutation critical path. Update the shared helper
in "`@/lib/queues/events`" around emitDashboardEvent/logDashboardEventError to use
a non-blocking delivery strategy or a bounded timeout, or move the dispatch so
the route handler returns immediately after the transaction commits. Keep the
post mutation path in the route handler independent of the event call so a
successful write is not delayed by dashboard event delivery.

Comment thread apps/cms/src/components/settings/fields/import.tsx
Comment thread apps/cms/src/components/settings/fields/import.tsx Outdated
Comment thread apps/jobs/src/utils/import-content.ts
Comment thread apps/jobs/src/utils/import-content.ts Outdated
Comment on lines +30 to +73
export async function getUniquePostSlug(
db: DbClient,
workspaceId: string,
preferredSlug: string
) {
const baseSlug = preferredSlug || "post";
let slug = baseSlug;
let suffix = 1;

while (
await db.post.findUnique({
where: { workspaceId_slug: { workspaceId, slug } },
select: { id: true },
})
) {
suffix += 1;
slug = `${baseSlug}-${suffix}`;
}

return slug;
}

/** Finds a unique author slug within the workspace by appending a numeric suffix. */
export async function getUniqueAuthorSlug(
db: DbClient,
workspaceId: string,
preferredSlug: string
) {
const baseSlug = preferredSlug || "imported-author";
let slug = baseSlug;
let suffix = 1;

while (
await db.author.findUnique({
where: { workspaceId_slug: { workspaceId, slug } },
select: { id: true },
})
) {
suffix += 1;
slug = `${baseSlug}-${suffix}`;
}

return slug;
}

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 | 🏗️ Heavy lift

These slug helpers are still race-prone.

Both functions do a read-then-write uniqueness loop outside the eventual create. importMarkdownFile() calls getUniquePostSlug() before tx.post.create(...), and getImportAuthor() computes an author slug before db.author.upsert(...), so a concurrent writer can still claim the same slug between the check and the write. The result is intermittent unique-constraint failures and failed imports under parallel jobs.

🤖 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/jobs/src/utils/import-records.ts` around lines 30 - 73, The slug
generation in getUniquePostSlug and getUniqueAuthorSlug is still vulnerable to
races because it checks uniqueness before the eventual create/upsert in
importMarkdownFile and getImportAuthor. Move slug resolution to the write path
or retry on unique-constraint failure so tx.post.create and db.author.upsert
cannot lose to a concurrent writer; keep the helper names as the central
location for this logic and ensure the final persisted slug is chosen
atomically.

Comment on lines 28 to +30
export type TaskMessage =
| { type: "export.process"; jobId: string }
| { type: "import.process"; jobId: string }
| { type: "import.create"; jobId: string };
| { type: "import.process"; jobId: string };

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep a temporary compatibility path for import.create.

This narrows the shared queue contract immediately, but apps/jobs/src/consumers/tasks.ts now throws on any unknown task type. Any already-enqueued import.create message during rollout will be retried/DLQ'd instead of finishing the import. Keep the old variant handling until the queue is drained, or explicitly migrate/purge outstanding messages before deploy.

🤖 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 `@packages/events/src/queue.ts` around lines 28 - 30, The shared queue contract
in TaskMessage has been narrowed too early, and tasks.ts now treats old
import.create messages as unknown. Keep backward-compatible handling for the
legacy import.create variant in the task consumer while the queue drains, or add
an explicit migration/purge step before removing it; make sure TaskMessage and
the consumer logic in tasks.ts stay aligned until no old messages remain.

div: ["data-twitter", "data-src", "data-youtube-video"],
span: ["style", "data-color"],
iframe: ["src", "allowfullscreen", "style", "width", "height"],
input: ["type", "checked"],

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve disabled on allowed checkboxes.

exclusiveFilter now keeps checkbox inputs, but allowedAttributes.input still strips disabled. That turns imported task-list checkboxes into live controls once sanitizeHtml() runs in apps/jobs/src/lib/import.ts, so stored post content no longer matches the Markdown source.

Suggested fix
-      input: ["type", "checked"],
+      input: ["type", "checked", "disabled"],

Also applies to: 104-105

🤖 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 `@packages/utils/src/functions/sanitize.ts` at line 68, The input sanitization
rule is still stripping the disabled state from kept checkbox inputs, so
imported task-list checkboxes become interactive after sanitizeHtml() runs.
Update the allowed input attributes in sanitize.ts so the
sanitizeHtml/exclusiveFilter path preserves disabled for checkbox inputs while
still allowing type and checked, and make sure any related attribute list
entries referenced by the comment are updated consistently.

@vercel vercel Bot temporarily deployed to Preview – marble-app June 27, 2026 16:50 Inactive
@taqh taqh marked this pull request as ready for review June 27, 2026 16:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if (job.status !== "queued" && job.status !== "processing") {
return;
}
if (job.status === "queued") {

P2 Badge Make import retries idempotent

When a queue delivery is retried after the worker has already created some posts but before the final importJob.update completes, the job remains processing; this guard explicitly allows processing jobs to run again. Since importMarkdownFile always inserts new ImportItems/posts and only suffixes slugs, the retry imports the same files a second time instead of resuming or skipping existing items.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/jobs/src/utils/import-content.ts Outdated

/** Extracts supported Markdown files from a zip archive in deterministic order. */
function parseMarkdownZipImport(bytes: Uint8Array) {
const entries = unzipSync(bytes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit ZIP inflation before filtering entries

For ZIP imports, unzipSync(bytes) inflates every entry before the .md/.mdx filter and before MAX_ZIP_EXTRACTED_BYTES is checked. An otherwise accepted 4 MB zip can contain a huge unsupported or hidden entry, so the Worker does all decompression and can run out of CPU/memory before the safeguards run; cap total uncompressed size during extraction or use streaming/lazy entry processing.

Useful? React with 👍 / 👎.

Comment on lines +80 to 83
if (activeWorkspace?.id) {
await queryClient.invalidateQueries({
queryKey: QUERY_KEYS.POSTS(workspaceId),
queryKey: QUERY_KEYS.IMPORTS(activeWorkspace.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.

P2 Badge Invalidate cached posts after queued imports

When imports are launched from the posts page, that page's React Query entry stays fresh for one hour (apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx:64-69). This success handler only invalidates IMPORTS, so after the worker creates drafts and the user follows “View posts”, the old list/empty state can be reused until refresh or stale time; invalidate the posts query when the import is started or when the polled job completes.

Useful? React with 👍 / 👎.

Comment thread apps/jobs/src/lib/import.ts Outdated
Comment on lines +72 to +76
categoryId,
publishedAt: now,
workspaceId: job.workspaceId,
authors: {
connect: [{ id: authorId }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply parsed frontmatter when creating drafts

For Markdown with category, tags, or author frontmatter, parseMarkdownImport records those values on the ImportItem, but the draft post created here always uses the fallback Uncategorized category and connects only the job creator, with no tag relation. Those imported drafts lose the metadata the import flow parsed, so ZIPs/single files with frontmatter don't round-trip taxonomy/author data.

Useful? React with 👍 / 👎.

@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.

Caution

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

⚠️ Outside diff range comments (1)
apps/jobs/src/scheduled/imports.ts (1)

109-113: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Base terminal-job retention on terminal timestamps, not createdAt.

A job that stays active longer than the retention window will match this query as soon as it flips to failed or completed, so the same cleanup run can delete it with effectively no terminal retention. Filter terminal jobs by failedAt/completedAt instead.

Suggested fix
   const oldJobs = await db.importJob.findMany({
     where: {
-      createdAt: { lt: retentionCutoff },
-      status: { in: ["completed", "failed"] },
+      OR: [
+        { status: "completed", completedAt: { lt: retentionCutoff } },
+        { status: "failed", failedAt: { lt: retentionCutoff } },
+      ],
     },
🤖 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/jobs/src/scheduled/imports.ts` around lines 109 - 113, The retention
query in the import cleanup path is using createdAt in the oldJobs lookup, which
can delete jobs immediately after they become terminal. Update the filtering in
the scheduled import cleanup logic to base terminal-job retention on the actual
terminal timestamps in db.importJob.findMany, using failedAt for failed jobs and
completedAt for completed jobs instead of createdAt, while keeping the existing
status filter and retentionCutoff behavior.
🧹 Nitpick comments (3)
apps/cms/src/app/api/data/import/route.ts (1)

94-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared logger instead of console.error.

These new failure paths log raw request/SDK errors directly from the route handler. Please send them through the app's structured/redacted logging path rather than console.*. As per coding guidelines, "Don't use console".

Also applies to: 146-152

🤖 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/cms/src/app/api/data/import/route.ts` around lines 94 - 95, The import
route is logging raw failures with console.error instead of the app’s structured
logger, so replace these error paths in the route handler with the shared
logging utility used by the CMS app. Update the failure handling in the import
upload flow and the other affected failure block referenced by the same route to
call the existing logger with redacted/structured error context rather than
console.*. Use the route handler and the import/upload helper functions in this
file to locate the affected spots.

Source: Coding guidelines

apps/jobs/src/scheduled/imports.ts (2)

71-106: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

This cleanup batch is fully serialized.

Each DB mutation and storage delete waits for the previous job, so one slow call stretches the whole batch. Consider Promise.allSettled with a small concurrency limit here. As per coding guidelines, "Don't use await inside loops".

Also applies to: 123-137

🤖 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/jobs/src/scheduled/imports.ts` around lines 71 - 106, The cleanup loop
in the scheduled imports job is fully serialized because `staleJobs` is
processed with sequential awaits inside the `imports` flow, which makes one slow
DB or storage call block the whole batch. Refactor the per-job work in this
section to run with bounded concurrency instead of awaiting inside the loop,
using a small limit and `Promise.allSettled`-style handling while preserving the
current `db.importJob.updateMany` and `deleteImportUpload` behavior and the
existing `staleCount`/`uploadKey` updates.

Source: Coding guidelines


140-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared logger instead of console.log.

Please route these cleanup summaries through the app logger rather than console.*. As per coding guidelines, "Don't use console".

🤖 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/jobs/src/scheduled/imports.ts` around lines 140 - 145, The cleanup
summary messages in the import job cleanup flow are still using console.log,
which violates the shared logging guideline. Update the logging in the imports
cleanup logic to route both the stale-count and deleted-count summaries through
the app’s shared logger instead of console, using the existing logger available
in the imports.ts cleanup path and the related cleanup function that emits these
messages.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@apps/jobs/src/scheduled/imports.ts`:
- Around line 109-113: The retention query in the import cleanup path is using
createdAt in the oldJobs lookup, which can delete jobs immediately after they
become terminal. Update the filtering in the scheduled import cleanup logic to
base terminal-job retention on the actual terminal timestamps in
db.importJob.findMany, using failedAt for failed jobs and completedAt for
completed jobs instead of createdAt, while keeping the existing status filter
and retentionCutoff behavior.

---

Nitpick comments:
In `@apps/cms/src/app/api/data/import/route.ts`:
- Around line 94-95: The import route is logging raw failures with console.error
instead of the app’s structured logger, so replace these error paths in the
route handler with the shared logging utility used by the CMS app. Update the
failure handling in the import upload flow and the other affected failure block
referenced by the same route to call the existing logger with
redacted/structured error context rather than console.*. Use the route handler
and the import/upload helper functions in this file to locate the affected
spots.

In `@apps/jobs/src/scheduled/imports.ts`:
- Around line 71-106: The cleanup loop in the scheduled imports job is fully
serialized because `staleJobs` is processed with sequential awaits inside the
`imports` flow, which makes one slow DB or storage call block the whole batch.
Refactor the per-job work in this section to run with bounded concurrency
instead of awaiting inside the loop, using a small limit and
`Promise.allSettled`-style handling while preserving the current
`db.importJob.updateMany` and `deleteImportUpload` behavior and the existing
`staleCount`/`uploadKey` updates.
- Around line 140-145: The cleanup summary messages in the import job cleanup
flow are still using console.log, which violates the shared logging guideline.
Update the logging in the imports cleanup logic to route both the stale-count
and deleted-count summaries through the app’s shared logger instead of console,
using the existing logger available in the imports.ts cleanup path and the
related cleanup function that emits these messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 786293d5-19d7-4604-aeb1-51ab4d10729d

📥 Commits

Reviewing files that changed from the base of the PR and between 6529af7 and 655bdbc.

📒 Files selected for processing (7)
  • apps/cms/src/app/api/data/import/route.ts
  • apps/cms/src/components/settings/fields/import.tsx
  • apps/cms/src/utils/import.ts
  • apps/jobs/src/lib/import.ts
  • apps/jobs/src/scheduled/imports.ts
  • apps/jobs/src/utils/import-content.ts
  • apps/jobs/src/utils/import-records.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/cms/src/components/settings/fields/import.tsx
  • apps/cms/src/utils/import.ts
  • apps/jobs/src/utils/import-content.ts
  • apps/jobs/src/lib/import.ts

@taqh taqh deleted the feat/add-data-imports branch July 13, 2026 22:16
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.

1 participant