Skip to content

Import endpoint: POST /api/v1/quotes/import #45

Description

@DutchJaFO

Add a dedicated bulk import endpoint, separate from the single-record write endpoints (#16).

Import is a distinct operation: it validates a batch, detects duplicates, resolves field-level conflicts, and commits atomically — none of which map cleanly onto repeated POST /api/v1/quotes calls.

Endpoint

POST /api/v1/quotes/import

Request

{
  "format": "json" | "csv",
  "enrich": false,
  "conflictStrategy": "skip" | "overwrite" | "review",
  "data": "<raw JSON array or CSV string>"
}

Alternatively, multipart/form-data with a file upload — both should be supported.

conflictStrategy

Controls what happens when an imported record matches an existing record (same quote + source text) but has different field values.

Strategy Behaviour
skip Keep existing record unchanged. Default — safe for automated imports.
overwrite Replace all differing fields with imported values, regardless of what is currently stored.
review Do not write conflicting records. Return the conflicts in the response for the user to resolve manually (see conflict resolution flow below).

A record with no field differences is always silently skipped — it is identical to what already exists.

enrich

Optional (default false). When true, runs the enrichment service (#19) on each successfully imported row after writing.

Response

{
  "imported": 10,
  "skipped": 2,
  "enriched": 7,
  "errors": [
    { "row": 4, "field": "type", "message": "Unknown type 'videogame'" }
  ],
  "conflicts": [
    {
      "importId": "client-assigned-id-or-row-index",
      "existingId": "uuid-of-existing-record",
      "fields": [
        { "field": "date", "existingValue": "1994", "importedValue": "1995" },
        { "field": "genres", "existingValue": ["drama"], "importedValue": ["drama", "thriller"] }
      ]
    }
  ]
}

conflicts is populated only when conflictStrategy: "review". Each conflict includes enough information for the UI to render a side-by-side comparison.

Conflict resolution endpoint

POST /api/v1/quotes/import/resolve

Accepts per-field resolutions for conflicts returned by a prior review import. The user decides each field independently.

{
  "resolutions": [
    {
      "existingId": "uuid",
      "fields": [
        { "field": "date", "resolution": "keep" | "replace" },
        { "field": "genres", "resolution": "keep" | "replace" }
      ]
    }
  ]
}

Each resolution is recorded in the audit log (#56) with ActorType='user', Action='updated', and old/new values — giving a clear record of who decided what and why.

General behaviour

  • IDs always generated server-side — never trusted from the payload
  • createdAt and updatedAt set to time of import — source timestamps ignored
  • Valid rows committed in a single transaction per batch; one bad row does not abort the batch
  • Unknown fields in the payload silently ignored
  • originalLanguage defaults to "en" if absent
  • When enrich: true, enrichment runs after commit; a failed lookup leaves the row as-is without rolling back the import

Formats supported

  • JSON — array of objects in the canonical quotes.json schema
  • CSV — columns: quote, source, author, character, type, genres, date, originalLanguage

Existing sources as the format reference

The two seed datasets (vilaboim/movie-quotes and NikhilNamal17/popular-movie-quotes) conform to the JSON schema after seed script normalisation. The import endpoint must accept that output directly.

Enrichment integration

Shared with the enrichment script (#19) via Quotinator.Enrichment. The import endpoint calls the same service when enrich: true. Design decision on library vs. project resolved in #19.

Auth

Requires API key authentication (#15) — same policy as write endpoints.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions