diff --git a/flake.nix b/flake.nix index f0196e1..7081e8e 100644 --- a/flake.nix +++ b/flake.nix @@ -53,6 +53,12 @@ # NBD task management nbd.packages.${system}.nbd + + # MCP servers + pkgs.nodejs + + # OpenAPI Linter + pkgs.redocly ]; shellHook = '' diff --git a/quotesdb/api/openapi.yaml b/quotesdb/api/openapi.yaml new file mode 100644 index 0000000..5ea483d --- /dev/null +++ b/quotesdb/api/openapi.yaml @@ -0,0 +1,393 @@ +openapi: "3.1.0" + +info: + title: QuotesDB API + description: A simple quotes database with passphrase-based quote ownership. + version: "0.1.0" + license: + name: MIT OR Apache-2.0 + +servers: + - url: http://localhost:8787 + description: Local development + - url: https://api.quotesdb.example.com + description: Production (Cloudflare Workers) + +# --------------------------------------------------------------------------- +# Security +# --------------------------------------------------------------------------- +# Auth is per-quote: each quote has a 4-word passphrase (auth_code) that was +# returned at creation time. Callers supply it via the X-Auth-Code header for +# mutating operations (POST /:id, DELETE /:id). +components: + securitySchemes: + AuthCode: + type: apiKey + in: header + name: X-Auth-Code + description: > + 4-word passphrase returned when the quote was created + (e.g. ocean-table-purple-storm). Required for update and delete. + + # ------------------------------------------------------------------------- + # Schemas + # ------------------------------------------------------------------------- + schemas: + + # Returned for all GET responses — auth_code intentionally omitted. + Quote: + type: object + required: + - id + - text + - author + - tags + - created_at + - updated_at + properties: + id: + type: string + description: NanoID (~21 characters). + example: "V1StGXR8_Z5jdHi6B-myT" + text: + type: string + description: The quote text. + example: "The only way to do great work is to love what you do." + author: + type: string + description: The person attributed with the quote. + example: "Steve Jobs" + source: + type: ["string", "null"] + description: Optional source (book, speech, etc.). + example: "Stanford Commencement Address, 2005" + date: + type: ["string", "null"] + format: date + description: Optional ISO 8601 date (YYYY-MM-DD) associated with the quote. + example: "2005-06-12" + tags: + type: array + items: + type: string + description: Zero or more tags attached to the quote. + example: ["work", "inspiration"] + created_at: + type: string + format: date-time + description: When the quote was first stored (UTC). + updated_at: + type: string + format: date-time + description: When the quote was last modified (UTC). + + # Returned only from the create (PUT) endpoint — includes auth_code. + QuoteCreated: + allOf: + - $ref: "#/components/schemas/Quote" + - type: object + required: + - auth_code + properties: + auth_code: + type: string + description: > + 4-word passphrase that authorises future edits and deletes. + Store this — it cannot be recovered later. + example: "ocean-table-purple-storm" + + # Request body for PUT /api/quotes (create). + QuoteCreateRequest: + type: object + required: + - text + - author + properties: + text: + type: string + description: The quote text. + author: + type: string + description: The person attributed with the quote. + source: + type: string + description: Optional source (book, speech, etc.). + date: + type: string + format: date + description: Optional ISO 8601 date (YYYY-MM-DD). + tags: + type: array + items: + type: string + description: Zero or more tags. + default: [] + auth_code: + type: string + description: > + Optional custom auth code. If omitted, a 4-word passphrase is + auto-generated by the server and returned in the response. + + # Request body for POST /api/quotes/:id (update — all fields optional). + QuoteUpdateRequest: + type: object + properties: + text: + type: string + description: Replacement quote text. + author: + type: string + description: Replacement author name. + source: + type: ["string", "null"] + description: Replacement source. Pass null to clear. + date: + type: ["string", "null"] + format: date + description: Replacement date. Pass null to clear. + tags: + type: array + items: + type: string + description: Replacement tag list. Replaces all existing tags. + + # Paginated list of quotes. + QuoteList: + type: object + required: + - quotes + - page + - total_pages + - total_count + properties: + quotes: + type: array + items: + $ref: "#/components/schemas/Quote" + page: + type: integer + minimum: 1 + description: Current page number. + total_pages: + type: integer + description: Total number of pages given the current filters. + total_count: + type: integer + description: Total number of quotes matching the current filters. + + # Standard error envelope used by all error responses. + Error: + type: object + required: + - error + properties: + error: + type: string + description: Human-readable error message. + example: "quote not found" + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +# IMPORTANT — router registration order for the Rust implementation: +# GET /api/quotes/random must be registered BEFORE GET /api/quotes/{id} +# to prevent "random" being matched as an id parameter. +paths: + + /api/: + get: + operationId: getOpenApiSpec + summary: OpenAPI specification + description: Returns this OpenAPI specification as JSON. + tags: [meta] + responses: + "200": + description: The OpenAPI spec in JSON format. + content: + application/json: + schema: + type: object + description: Raw OpenAPI 3.1 document. + + /api/quotes: + get: + operationId: listQuotes + summary: List quotes + description: Returns a paginated list of quotes, optionally filtered by author or tag. + tags: [quotes] + parameters: + - name: page + in: query + description: Page number (1-based). + required: false + schema: + type: integer + minimum: 1 + default: 1 + - name: author + in: query + description: Filter by exact author name (case-insensitive). + required: false + schema: + type: string + - name: tag + in: query + description: Filter to quotes that have this tag. + required: false + schema: + type: string + responses: + "200": + description: Paginated list of quotes. + content: + application/json: + schema: + $ref: "#/components/schemas/QuoteList" + + put: + operationId: createQuote + summary: Create a quote + description: > + Creates a new quote. If auth_code is omitted from the request body, + the server auto-generates a 4-word passphrase and returns it in the + response. Store the auth_code — it cannot be recovered later. + tags: [quotes] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QuoteCreateRequest" + responses: + "201": + description: Quote created successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/QuoteCreated" + "422": + description: Validation error (e.g. missing required fields). + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + # NOTE: registered before /api/quotes/{id} in the Rust router. + /api/quotes/random: + get: + operationId: getRandomQuote + summary: Random quote + description: Returns a single randomly selected quote. + tags: [quotes] + responses: + "200": + description: A random quote. + content: + application/json: + schema: + $ref: "#/components/schemas/Quote" + "404": + description: No quotes exist in the database. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /api/quotes/{id}: + parameters: + - name: id + in: path + required: true + description: NanoID of the quote (~21 characters). + schema: + type: string + example: "V1StGXR8_Z5jdHi6B-myT" + + get: + operationId: getQuote + summary: Get a quote by ID + description: Returns a single quote identified by its NanoID. + tags: [quotes] + responses: + "200": + description: The requested quote. + content: + application/json: + schema: + $ref: "#/components/schemas/Quote" + "404": + description: Quote not found. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + post: + operationId: updateQuote + summary: Update a quote + description: > + Partially updates an existing quote. Only the fields included in the + request body are modified. Requires the quote's auth_code via the + X-Auth-Code header. + tags: [quotes] + security: + - AuthCode: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QuoteUpdateRequest" + responses: + "200": + description: The updated quote. + content: + application/json: + schema: + $ref: "#/components/schemas/Quote" + "403": + description: Auth code missing or incorrect. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Quote not found. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + delete: + operationId: deleteQuote + summary: Delete a quote + description: > + Permanently deletes a quote. Requires the quote's auth_code via the + X-Auth-Code header. + tags: [quotes] + security: + - AuthCode: [] + responses: + "204": + description: Quote deleted successfully. No response body. + "403": + description: Auth code missing or incorrect. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Quote not found. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + +# --------------------------------------------------------------------------- +# Tags (for grouping in generated docs) +# --------------------------------------------------------------------------- +tags: + - name: meta + description: API metadata endpoints. + - name: quotes + description: CRUD operations on quotes.