mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
docs(api): describe cover letter endpoints (#3509)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
232f48578b
commit
fbf1f8fbac
@@ -1,3 +1,4 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import z from "zod";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
@@ -16,6 +17,11 @@ type GeneratedSpecView = {
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
tags?: string[];
|
||||
operationId?: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
responses?: Record<string, { description?: string }>;
|
||||
requestBody?: {
|
||||
content?: Record<string, { schema?: unknown }>;
|
||||
};
|
||||
@@ -72,6 +78,51 @@ function findImpossibleRequestSchemas(spec: GeneratedSpecView) {
|
||||
}
|
||||
|
||||
describe("generateOpenApiSpec", () => {
|
||||
it("documents all cover-letter procedures with REST metadata", async () => {
|
||||
const spec = (await generateSpec()) as GeneratedSpecView;
|
||||
const expected = [
|
||||
["get", "/cover-letters", "listCoverLetters", "List cover letters", "200"],
|
||||
["get", "/cover-letters/{id}", "getCoverLetter", "Get cover letter by ID", "200"],
|
||||
["post", "/cover-letters", "createCoverLetter", "Create a cover letter", "200"],
|
||||
["put", "/cover-letters/{id}", "updateCoverLetter", "Update a cover letter", "200"],
|
||||
["post", "/cover-letters/{id}/refresh-style", "refreshCoverLetterStyle", "Refresh cover letter style", "200"],
|
||||
["post", "/cover-letters/{id}/duplicate", "duplicateCoverLetter", "Duplicate a cover letter", "200"],
|
||||
["delete", "/cover-letters/{id}", "deleteCoverLetter", "Delete a cover letter", "200"],
|
||||
["post", "/cover-letters/from-resume", "copyEmbeddedCoverLetter", "Copy an embedded cover letter", "200"],
|
||||
["get", "/cover-letters/{id}/export", "exportCoverLetter", "Export a cover letter", "200"],
|
||||
["post", "/cover-letters/import", "importCoverLetter", "Import a cover letter", "200"],
|
||||
] as const;
|
||||
|
||||
for (const [method, path, operationId, summary, successStatus] of expected) {
|
||||
const operation = spec.paths?.[path]?.[method];
|
||||
expect(operation).toMatchObject({
|
||||
tags: ["Cover Letters"],
|
||||
operationId,
|
||||
summary,
|
||||
description: expect.any(String),
|
||||
responses: { [successStatus]: { description: expect.any(String) } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps published cover-letter operations in sync with the runtime spec", async () => {
|
||||
const published = JSON.parse(
|
||||
await readFile(new URL("../../../../docs/spec.json", import.meta.url), "utf8"),
|
||||
) as GeneratedSpecView;
|
||||
const runtime = await generateSpec();
|
||||
const coverLetterPaths = (spec: GeneratedSpecView) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(spec.paths ?? {}).filter(
|
||||
([path]) => path.startsWith("/cover-letters") || path.startsWith("/coverLetters/"),
|
||||
),
|
||||
);
|
||||
|
||||
const publishedPaths = coverLetterPaths(published);
|
||||
const runtimePaths = coverLetterPaths(runtime as GeneratedSpecView);
|
||||
expect(Object.keys(publishedPaths).sort()).toEqual(Object.keys(runtimePaths).sort());
|
||||
expect(publishedPaths).toEqual(runtimePaths);
|
||||
});
|
||||
|
||||
it("uses caller-provided application URL and version", async () => {
|
||||
const spec = await generateSpec();
|
||||
|
||||
|
||||
@@ -61,3 +61,43 @@ description: "Create Reactive Resume API keys, authenticate REST requests with b
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Cover-letter REST endpoint migration
|
||||
|
||||
Cover-letter REST operations now use `/cover-letters` resource paths and HTTP methods instead of the automatically generated `/coverLetters/*` POST endpoints. This is a **breaking change for REST clients**: the old REST URLs are no longer served and return `404`. There are no compatibility aliases.
|
||||
|
||||
All paths below are relative to `/api/openapi` on your instance. Authentication with `x-api-key` is unchanged.
|
||||
|
||||
| Previous endpoint | Replacement endpoint |
|
||||
| --- | --- |
|
||||
| `POST /coverLetters/list` | `GET /cover-letters` |
|
||||
| `POST /coverLetters/getById` | `GET /cover-letters/{id}` |
|
||||
| `POST /coverLetters/create` | `POST /cover-letters` |
|
||||
| `POST /coverLetters/update` | `PUT /cover-letters/{id}` |
|
||||
| `POST /coverLetters/refreshStyle` | `POST /cover-letters/{id}/refresh-style` |
|
||||
| `POST /coverLetters/duplicate` | `POST /cover-letters/{id}/duplicate` |
|
||||
| `POST /coverLetters/delete` | `DELETE /cover-letters/{id}` |
|
||||
| `POST /coverLetters/copyEmbedded` | `POST /cover-letters/from-resume` |
|
||||
| `POST /coverLetters/export` | `GET /cover-letters/{id}/export` |
|
||||
| `POST /coverLetters/import` | `POST /cover-letters/import` |
|
||||
|
||||
Update request inputs as well as the URL and method:
|
||||
|
||||
- Move `id` from the JSON body into the URL path wherever `{id}` appears. URL-encode the ID as a single path segment.
|
||||
- For listing, send `search`, `resumeId`, `applicationId`, `limit`, and `offset` as query parameters, not a JSON body. The get-by-ID and export operations also have no request body.
|
||||
- Keep the remaining inputs as JSON for POST, PUT, and DELETE requests, with `Content-Type: application/json`. In particular, `expectedRevision` remains required in the JSON body for update, refresh-style, and delete; refreshing style also requires `resumeId`.
|
||||
- Create, copy-from-resume, and import keep their existing JSON inputs. Update still modifies only the supplied fields; it does not replace the entire document.
|
||||
- Successful operations return `200`, including create and delete. Delete has an empty response body; do not expect `204` or parse a JSON document from it. Other response shapes are unchanged.
|
||||
|
||||
For example, list cover letters with query parameters:
|
||||
|
||||
```bash
|
||||
curl --get "https://rxresu.me/api/openapi/cover-letters" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
--data-urlencode "search=Engineer" \
|
||||
--data-urlencode "limit=20"
|
||||
```
|
||||
|
||||
If you generate an SDK, regenerate it from your instance's `/api/openapi/spec.json` after upgrading: operation IDs have changed too (for example, `coverLetters.getById` is now `getCoverLetter`). Self-hosted clients should use the spec from the version they are running.
|
||||
|
||||
This migration applies only to REST under `/api/openapi`. Existing oRPC clients under `/api/rpc` continue to use the same `coverLetters.*` procedure names and RPC protocol; do not apply the REST path or payload changes to them.
|
||||
|
||||
+11279
-10807
File diff suppressed because it is too large
Load Diff
@@ -4,42 +4,133 @@ import { coverLetterService } from "./service";
|
||||
|
||||
export const coverLettersRouter = {
|
||||
list: protectedProcedure
|
||||
.route({
|
||||
method: "GET",
|
||||
path: "/cover-letters",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "listCoverLetters",
|
||||
summary: "List cover letters",
|
||||
description:
|
||||
"Returns the authenticated user's saved cover letters, optionally filtered by search, resume, or application.",
|
||||
successDescription: "A paginated list of cover letters.",
|
||||
})
|
||||
.input(coverLetterDto.list.input)
|
||||
.output(coverLetterDto.list.output)
|
||||
.handler(({ context, input }) => coverLetterService.list({ ...input, userId: context.user.id })),
|
||||
getById: protectedProcedure
|
||||
.route({
|
||||
method: "GET",
|
||||
path: "/cover-letters/{id}",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "getCoverLetter",
|
||||
summary: "Get cover letter by ID",
|
||||
description: "Returns a single saved cover letter belonging to the authenticated user.",
|
||||
successDescription: "The cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.getById.input)
|
||||
.output(coverLetterDto.getById.output)
|
||||
.handler(({ context, input }) => coverLetterService.getById({ ...input, userId: context.user.id })),
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/cover-letters",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "createCoverLetter",
|
||||
summary: "Create a cover letter",
|
||||
description: "Creates a saved cover letter, optionally linked to a resume or job application.",
|
||||
successDescription: "The newly created cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.create.input)
|
||||
.output(coverLetterDto.create.output)
|
||||
.handler(({ context, input }) => coverLetterService.create({ ...input, userId: context.user.id })),
|
||||
update: protectedProcedure
|
||||
.route({
|
||||
method: "PUT",
|
||||
path: "/cover-letters/{id}",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "updateCoverLetter",
|
||||
summary: "Update a cover letter",
|
||||
description: "Updates the supplied fields of a saved cover letter using its expected revision.",
|
||||
successDescription: "The updated cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.update.input)
|
||||
.output(coverLetterDto.update.output)
|
||||
.handler(({ context, input }) => coverLetterService.update({ ...input, userId: context.user.id })),
|
||||
refreshStyle: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/cover-letters/{id}/refresh-style",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "refreshCoverLetterStyle",
|
||||
summary: "Refresh cover letter style",
|
||||
description: "Refreshes a cover letter's style from a selected resume while preserving its content and template.",
|
||||
successDescription: "The cover letter with refreshed style.",
|
||||
})
|
||||
.input(coverLetterDto.refreshStyle.input)
|
||||
.output(coverLetterDto.refreshStyle.output)
|
||||
.handler(({ context, input }) => coverLetterService.refreshStyle({ ...input, userId: context.user.id })),
|
||||
duplicate: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/cover-letters/{id}/duplicate",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "duplicateCoverLetter",
|
||||
summary: "Duplicate a cover letter",
|
||||
description: "Creates a copy of an existing saved cover letter, optionally with a new name.",
|
||||
successDescription: "The duplicated cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.duplicate.input)
|
||||
.output(coverLetterDto.duplicate.output)
|
||||
.handler(({ context, input }) => coverLetterService.duplicate({ ...input, userId: context.user.id })),
|
||||
delete: protectedProcedure
|
||||
.route({
|
||||
method: "DELETE",
|
||||
path: "/cover-letters/{id}",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "deleteCoverLetter",
|
||||
summary: "Delete a cover letter",
|
||||
description: "Permanently deletes a saved cover letter using its expected revision.",
|
||||
successDescription: "The cover letter was deleted successfully.",
|
||||
})
|
||||
.input(coverLetterDto.delete.input)
|
||||
.output(coverLetterDto.delete.output)
|
||||
.handler(({ context, input }) => coverLetterService.delete({ ...input, userId: context.user.id })),
|
||||
copyEmbedded: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/cover-letters/from-resume",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "copyEmbeddedCoverLetter",
|
||||
summary: "Copy an embedded cover letter",
|
||||
description: "Creates a saved cover letter by copying an embedded cover-letter item from a resume.",
|
||||
successDescription: "The newly created cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.copyEmbedded.input)
|
||||
.output(coverLetterDto.copyEmbedded.output)
|
||||
.handler(({ context, input }) => coverLetterService.copyEmbedded({ ...input, userId: context.user.id })),
|
||||
export: protectedProcedure
|
||||
.route({
|
||||
method: "GET",
|
||||
path: "/cover-letters/{id}/export",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "exportCoverLetter",
|
||||
summary: "Export a cover letter",
|
||||
description: "Exports a saved cover letter as a standalone Reactive Resume cover-letter document.",
|
||||
successDescription: "The exported cover letter document.",
|
||||
})
|
||||
.input(coverLetterDto.export.input)
|
||||
.output(coverLetterDto.export.output)
|
||||
.handler(({ context, input }) => coverLetterService.export({ ...input, userId: context.user.id })),
|
||||
import: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/cover-letters/import",
|
||||
tags: ["Cover Letters"],
|
||||
operationId: "importCoverLetter",
|
||||
summary: "Import a cover letter",
|
||||
description: "Creates a saved cover letter from an exported Reactive Resume cover-letter document.",
|
||||
successDescription: "The imported cover letter.",
|
||||
})
|
||||
.input(coverLetterDto.import.input)
|
||||
.output(coverLetterDto.import.output)
|
||||
.handler(({ context, input }) => coverLetterService.import({ ...input, userId: context.user.id })),
|
||||
|
||||
Reference in New Issue
Block a user