12 KiB
Add @suaveplan/api-plugin-import
Package: @suaveplan/api-plugin-import
Status: PROPOSED
Type: new-package
- Design: ./design.md (omitted — every architectural decision lives in the spec delta's Implementation Notes references and in the per-feature
.mdfiles) - Spec delta: ./specs/api-plugin-import/spec.md
Why
Genesis needs a tabular-data ingest plugin for @suaveplan/api so user-facing apps can accept CSV / JSON / XLSX uploads, preview them, apply column mappings + per-row transforms + field validations, run dry-runs, persist import jobs, and stream progress events. The upstream
@suaveplan/api-plugin-import@1.1.7 (/mnt/s/web/suaveplan/packages/server/api-plugin-import, 2,320 LOC, ~98.72 % historical coverage) carries the entire feature surface but ships in a half-migrated state — two parallel ImportService implementations, seven @deprecated factory helpers, no definePlugin() export, an inline ad-hoc ImportError next to a proper BaseError-backed one, and a route bundle that hand-rolls Hono context casts.
This change lands a single, enterprise-grade @suaveplan/api-plugin-import@0.1.0 that consolidates the two services, removes every deprecated alias, wires a real createImportPlugin() onto the genesis PluginDescriptor from @suaveplan/api-plugin, gates routes through requireAuth() from @suaveplan/api-plugin-auth, scopes job lookups to c.get("user").id with 404 anti-enumeration, ships OTel instruments asserted with a real in-memory MeterProvider, and rewrites every test against @suaveplan/testing/runner.
What changes
- ADDED:
IMPORT_CODES/ImportError—defineCodes("IMPORT", […])registry of nine codes (NOT_FOUND,FORBIDDEN,VALIDATION,FILE_TOO_LARGE,UNSUPPORTED_FORMAT,PROCESSING_FAILED,STORAGE_ERROR,TIMEOUT,INVALID_OPTIONS) extendingSuaveplanError. - ADDED:
parseCsv/parseCsvLine/detectColumns/streamParseCsv/detectHeaders/parseStreamLine/applyColumnMappings— CSV utilities incl. quoted-field handling, custom delimiter, header detection, embedded-newline tolerance. - ADDED:
parseJson— JSON-array parser with non-array rejection. - ADDED:
parseXlsx— TSV-style XLSX parser (lightweight; documented limitation). - ADDED:
coerceType/validateField/transformRow/transformRows— declarative transform pipeline with type coercion (number/boolean/date), required/default-value handling, per-field validation (required/min/max/pattern/custom), computed fields. - ADDED:
ImportProgressTracker— real-time row tracker with rows/sec, ETA, percent-complete, batch helpers, listener fan-out. - ADDED:
MemoryImportStore+ImportStoreAdapter— async CRUD + list-by-status interface; in-memory implementation for tests and small deployments. - ADDED:
ImportService(canonical) —EventEmitter<ImportEventMap>-based service withpreview/startImport/getJob/getResults/listJobs/deleteJob, batch progress emission, duplicate-skip,onRowfilter, OTel instrumentation, scoped-by-owner store writes. - ADDED:
createImportRoutes— 5 OpenAPI routes (POST /preview,POST /,GET /,GET /{id},DELETE /{id}) emitting RFC 7807 problem+json on failure, validating bodies via Zod, scoping all reads / writes to the caller'sPrincipal.id. - ADDED:
createImportPlugin—definePlugin<ImportPluginConfig>factory wrapping the genesisPluginDescriptor(NOT the upstreamdefinePluginfrom@suaveplan/api) with 4-phase lifecycle, registeringimportServiceon theServiceRegistry, mounting routes under a configurablebasePath, optional-depending onauth/events/database. - ADDED:
respondProblemJson/PROBLEM_JSON_CONTENT_TYPE— RFC 7807 helper bridgingSuaveplanError(and unknown throwables) to aResponse. - ADDED:
getImportInstruments/resetImportInstruments/ImportInstruments— OTel counter+histogram bundle (import.jobs_total,import.rows_total,import.row_errors_total,import.duration_ms) with cache reset for test rebinding.
Excluded from this migration
The genesis 0.1.0 ships at functional parity with the upstream library surface
(parsers + transform pipeline + progress + memory store + service + routes +
plugin). The §3 enterprise-doc enhancements that depend on packages not yet
landed on genesis are deferred — each with a concrete architectural
justification — and are NOT silently dropped:
SurrealImportStore— requires@suaveplan/surrealdbmigrations + the Surreal-side schema. Genesis shipsMemoryImportStoreagainst the sameImportStoreAdapterinterface so the SurrealDB adapter lands as a sibling package without breaking consumers. Listed in pending follow-ups.- Rate-limit + idempotency + audit middleware — depends on
@suaveplan/api-plugin-rate-limit,@suaveplan/idempotency,@suaveplan/api-plugin-audit. These are wired by the host app via the standardapp.use(prefix, middleware)Hono pattern; the plugin does NOT hard-depend on them, so excluding them from THIS migration does not block consumers from layering them on top. - Streaming
Readable/AsyncIterableinput —streamParseCsvis shipped and the publicstartImportaccepts astring(parity with upstream). A follow-up will extend the input union tostring | AsyncIterable<string>without breaking the existing call site. - Benchmarks under
src/__benchmarks__/— every benchmark requires a mitata harness; the harness lives in@suaveplan/testingbut the benchmark gate is monorepo-wide and shipped per-package in a separate benchmark wave. - Foreign-key resolution, parallel batch workers, resumable checkpoints, S3/URL source adapters, SSE progress push — every item is a §4 future enhancement, not a §2 current-state capability. None are part of the upstream public surface; deferring them is parity, not regression.
Out of scope
- Modifying any other
packages/api/api-plugin-*/package,packages/messaging/*,packages/audio/*,packages/api/api-migrations/,packages/database/surrealdb/,packages/core/*,MANIFEST.md,STATUS.md,CLAUDE.md, or the rootpackage.json(exceptbun.lock). Concurrent sessions own those scopes. - The 7
@deprecatedfactory helpers (jobNotFound,invalidFormat,importFailed,validationError,parseError,mappingError,duplicateDetected). Genesis bans@deprecatedin source perAGENTS.md §2; the canonical helpers (importNotFound,importUnsupportedFormat,importProcessingFailed,importValidation) ship as the only API. - The legacy
services/import.service.tsimplementation. Genesis ships exactly one canonicalImportServiceper upstreamenterprise/api-plugin-import.md§3 row 1.
Source mapping
This change derives from /mnt/s/web/suaveplan/enterprise/api-plugin-import.md.
| Requirement | Source lines | Notes |
|---|---|---|
Single canonical ImportService |
L46 (§3 row 1) | "Converge on one ImportService" — genesis ships the canonical EventEmitter-based one |
createImportPlugin() wiring routes |
L47-L48 (§3 row 2) | Wraps onto genesis PluginDescriptor (not upstream definePlugin) |
No @deprecated helpers |
L49 (§3 row 3) + L37, L40 | Enforced by AGENTS.md §2 — grep gate is zero-match |
| XLSX support (TSV interchange) | L50 (§3 row 4) + upstream src/parsers/xlsx.parser.ts |
Genesis ships TSV-style parity; binary XLSX is documented limitation |
Async ImportStoreAdapter + MemoryImportStore |
L29, L51 (§3 row 5) | SurrealDB adapter deferred per ### Excluded |
| Streaming CSV parser | L26, L52 (§3 row 6) | streamParseCsv shipped; string input retained for parity |
Auth-gated routes via requireAuth() |
L53 (§3 row 7) | Genesis @suaveplan/api-plugin-auth substitutes for upstream @suaveplan/jwt-tenant |
| Owner-scoped jobs with 404 anti-enumeration | L53 (§3 row 7) + path-A safety doc | Foreign job lookups return 404 not 403 |
| OTel instruments | L57 (§3 row 9) | import.jobs_total, import.rows_total, import.row_errors_total, import.duration_ms |
Structured logger via LoggerLike |
L57 (§3 row 9) | Uses injected ctx.logger |
| RFC 7807 problem+json mapping | upstream src/routes/problem-json.ts |
Genesis re-renders via SuaveplanError.context.statusCode |
Stable IMPORT.* error registry |
L30, L37, L106 | defineCodes("IMPORT", […]) — 9 codes |
ImportProgressTracker with rows/sec + ETA |
upstream src/progress/import-progress.ts |
Verbatim functional parity |
transformRow / transformRows / coerceType / validateField |
upstream src/transform/transform-pipeline.ts |
Verbatim functional parity |
parseCsv / parseCsvLine / detectColumns / parseJson / streamParseCsv |
upstream src/types/import.types.ts + src/parsers/stream-parser.ts |
Verbatim functional parity |
| 5 OpenAPI routes (preview, create, list, get, delete) | upstream src/routes/import.routes.ts |
Owner scoping is new; route set is verbatim |
ImportError extends SuaveplanError |
upstream src/errors/import-errors.ts + AGENTS.md §2 |
Genesis uses SuaveplanError, not upstream BaseError |
ImportEventMap + EventEmitter |
upstream src/services/import-service.ts L30 |
Verbatim |
ImportJobRecord async store interface |
upstream src/store/import-store.ts |
Verbatim shape |
| Subpath exports per feature module | AGENTS.md §6, repo convention |
Per-feature ./<dir>/<file> entries |
Requirements with no source line
| Requirement | Justification |
|---|---|
| 100 % istanbul coverage thresholds | Genesis-wide policy (AGENTS.md §7) |
**Verified by:** lines at draft time |
Genesis-wide policy (AGENTS.md §13) |
Test imports via @suaveplan/testing/runner |
Genesis-wide policy (CLAUDE.md rule 11) |
| Explicit named re-exports in feature barrels | Genesis-wide policy (AGENTS.md §6a) |
Verification approach
Each requirement in the spec delta lists concrete it(...) titles under
**Verified by:**. Test files:
src/error-codes/error-codes.test.ts— registry shape, every code thrown.src/parsers/csv.test.ts— quoted fields, embedded newlines, custom delimiters, no headers, empty input.src/parsers/json.test.ts— array, non-array reject, scalar wrap.src/parsers/xlsx.test.ts— TSV split, no headers, trim behaviour.src/transform/pipeline.test.ts— every coerce branch, every validation rule, computed fields, transform failure rollback.src/progress/tracker.test.ts— single + batch increments, rows/sec, ETA, percent, listener fan-out, isComplete edge case.src/store/memory.test.ts— CRUD, list filter, missing-id error.src/service/service.test.ts— preview, startImport happy path, dry-run, skipDuplicates, onRow, batch progress emit, owner scoping, format validation, parse failure → IMPORT.PROCESSING_FAILED, mapping required, getJob 404.src/routes/problem-json.test.ts— SuaveplanError → status from context; unknown error → 500; HttpError shape.src/routes/build-routes.test.ts— every route happy path + 401 (no auth) + 404 (foreign job).src/otel/instruments.test.ts— cached bundle, reset rebinds, realMeterProvidercollects emitted metrics.src/plugin/plugin.test.ts— descriptor shape,INVALID_OPTIONSon bad config,onInitregisters service,onReadymounts routes,onCloseclears state.src/plugin/plugin.spec-conformance.test.ts— generics threaded, Zod boundary, every code reachable, OTel emit asserted, lifecycle dedup, no retroactive ticks.
Migration impact
None — additive only. No genesis package depends on
@suaveplan/api-plugin-import today (it is being introduced). The plugin
opts-in to @suaveplan/api-plugin-auth via optionalDependencies, so apps
that don't ship auth still register; their requireAuth() calls simply
throw AUTH.UNAUTHENTICATED per the auth plugin's contract.
Impact
Affected dependents
bunx turbo ls --filter=...@suaveplan/api-plugin-import enumerates zero
dependents — this is a fresh package.
- No dependents (verified by
bunx turbo ls --filter=...@suaveplan/api-plugin-import).