Skip to content

feat(csv-import-export): server side for CSV import and export - #66

Merged
fbritoferreira merged 10 commits into
feat/csv-import-exportfrom
feat/csv-import-export-server
Oct 9, 2026
Merged

fbritoferreira merged 10 commits into
feat/csv-import-exportfrom
feat/csv-import-export-server

Conversation

@fbritoferreira

Copy link
Copy Markdown
Owner

Summary

Server half of the CSV import/export plugin. Stacked on #65. The package stays private.

POST /import/:uid  (one batch, ≤ 500 rows)
  validateImportRequest          # match field unique + mapped, relations have matchOn, jobId unless dry run
  requireCan create/update(/publish) per mapped field, in the request locale
  requireCan read on each relation target's matchOn field
  importBatch
    loadIndex per relation/media column        # one $in query per column per batch
    prepareRow: unescape ' → coerce → resolve relations
    if onMissingRelation = fail and any target missing → write nothing, aborted
    db.query(uid).findMany(matchField $in keys)  # every locale, draft + published
    per row: no key / no match → create, one match → update, several → error
             publish after write when status = published and D&P is on
  jobs.record(counts, failed + skipped rows only)
server/src/
├── services/coerce.ts       # CSV cell → attribute value; strips the export's formula quote
├── services/schema.ts       # importable collections and fields (no components, private, password, morph)
├── services/validate.ts     # request validation
├── services/relations.ts    # relation/media lookup index
├── services/importer.ts     # batch upsert through the Document Service
├── services/exporter.ts     # paged export, papaparse with formula escaping
├── services/jobs.ts         # plugin::csv-import-export.job log
├── services/permissions.ts  # field- and locale-aware Content Manager checks
├── controllers/csv.ts, routes/index.ts, bootstrap.ts, config/index.ts

Permissions: ability.can(action, uid) on a bare type ignores field limits and rule conditions, so every check runs against a stand-in entry { locale }, once per field. Field-level and i18n locale limits apply; roles limited by "is creator" are refused.

Export returns { data: { fileName, rowCount, csv } } rather than a text/csv attachment: useFetchClient().post in @strapi/admin 5.57 ignores responseType and always parses JSON.

Evidence

  • Before: no server code.
    After: 105 tests pass (pnpm --filter @fbritoferreira/strapi-csv-import-export test); lint, typecheck, build, package:check pass; smoke prints smoke ok: tarball loads, 8 routes registered.
  • Tests that pin the risky inputs:
same match value twice in one batch      → create, then update the entry just created
" 5 " against integer column 5           → update (keys trimmed, compared as text)
onMissingRelation=fail, last row missing → aborted, zero writes
type without D&P or i18n                 → no locale passed, publish never called
"=1+1\nx" exported                       → "'=1+1\nx" (papaparse's own regex misses this)
"'-draft" imported                       → slug "-draft" (round trip)
role may update title only, maps views   → 403 "... for slug, views in locale en"
batch with another user's jobId          → 400, nothing written
  • End to end against real Strapi 5.57 in demo/ (PR 4): first import created 2, updated 1, second import created 0, updated 3; a role limited to title exports title with 200 and views with 403.

Merge Danger

Door: two-way

Blast Radius: none

Private package. The job content type only exists in apps that enable the plugin, which nothing does until PR 4's demo.

…missions, hide private attributes, tie batches to their job

userAbility.can(action, uid) ignores field limits and rule conditions. Checks now run against a stand-in entry with the request's locale, once per mapped or exported field, so field-level and i18n locale limits apply and creator-only roles are refused. Linking or exporting a relation by a target field needs read permission on that field of the target collection.

Private attributes are no longer importable, exportable or usable as relation match fields. Real imports must name a running import job owned by the caller. Cells the export prefixed with ' before a formula character are read back without the quote, so exports round-trip.
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8838fa58-5046-4bb2-bfdf-9ec25d5760c9

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​papaparse@​5.5.21001007482100
Addedpapaparse@​5.7.01001009589100

View full report

@fbritoferreira
fbritoferreira added this pull request to stack #69 October 9, 2026 19:50
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Test Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 100% (🎯 100%) 1361 / 1361
🟢 Statements 100% (🎯 100%) 1600 / 1600
🟢 Functions 100% (🎯 100%) 299 / 299
🟢 Branches 100% (🎯 100%) 1360 / 1360
File CoverageNo changed files found.
Generated in workflow #112 for commit 54406aa by the Vitest Coverage Report Action

@fbritoferreira
fbritoferreira merged commit 66cd76c into main Oct 9, 2026
11 checks passed
@fbritoferreira
fbritoferreira deleted the feat/csv-import-export-server branch October 9, 2026 19:54
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