mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
feat(schema): structured resume dates with a read-time upgrade and dual write
Dated entries and roles carry dates (start, end, present, and raw when the text couldn't be read exactly). The parser moves to the schema package and reports how each date was written; parseResumeData fills dates, infers the date format from how dates were typed and rewrites the legacy period/date text from them, so older clients and API readers keep working. API writes sync against the stored data, so an edit to the text alone is read back into dates. getById and getBySlug return upgraded data, ATS date rules and sorting read dates, JSON Resume and LinkedIn imports map their dates directly, entry titles may be empty (drafts aren't printed), and the MCP schema resource is generated live in place of the stale schema.json.
This commit is contained in:
@@ -2,6 +2,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { SEMANTIC_CSS_LIMITS_V1 } from "@reactive-resume/resume/stylesheet";
|
||||
import { parseResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { syncResumeDates, upgradeResumeDates } from "@reactive-resume/schema/resume/dates";
|
||||
import { parseResumeDataForWrite } from "@reactive-resume/schema/resume/write";
|
||||
|
||||
function parseApiResumeData(data: unknown, code: "BAD_REQUEST" | "INTERNAL_SERVER_ERROR", message: string): ResumeData {
|
||||
@@ -21,8 +22,17 @@ function parseApiResumeData(data: unknown, code: "BAD_REQUEST" | "INTERNAL_SERVE
|
||||
}
|
||||
}
|
||||
|
||||
export const parseWritableResumeData = (data: unknown) =>
|
||||
parseApiResumeData(data, "BAD_REQUEST", "Resume data does not match the canonical schema.");
|
||||
/**
|
||||
* Validates data before it's saved, then keeps structured dates and their legacy text in step. `previous` is
|
||||
* the stored data being replaced, which tells an edit to the text alone (from a client that doesn't know
|
||||
* structured dates) from an edit to the dates.
|
||||
*/
|
||||
export const parseWritableResumeData = (data: unknown, previous?: ResumeData) => {
|
||||
const parsed = parseApiResumeData(data, "BAD_REQUEST", "Resume data does not match the canonical schema.");
|
||||
upgradeResumeDates(parsed);
|
||||
syncResumeDates(parsed, previous);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const parseStoredResumeData = (data: unknown) =>
|
||||
parseApiResumeData(data, "INTERNAL_SERVER_ERROR", "Stored resume data does not match the canonical schema.");
|
||||
|
||||
@@ -140,9 +140,10 @@ async function applyResumePatchTx(
|
||||
input.operations.forEach(assertValidPatchPointers);
|
||||
|
||||
let patchedData: ResumeData;
|
||||
const storedData = parseStoredResumeData(existing.data);
|
||||
|
||||
try {
|
||||
patchedData = applyResumePatches(parseStoredResumeData(existing.data), input.operations);
|
||||
patchedData = applyResumePatches(storedData, input.operations);
|
||||
} catch (error) {
|
||||
if (error instanceof ResumePatchError) {
|
||||
throw new ORPCError("INVALID_PATCH_OPERATIONS", {
|
||||
@@ -158,7 +159,7 @@ async function applyResumePatchTx(
|
||||
});
|
||||
}
|
||||
|
||||
patchedData = parseWritableResumeData(patchedData);
|
||||
patchedData = parseWritableResumeData(patchedData, storedData);
|
||||
// The version guard is the ms-precision JS check above, under the SELECT ... FOR UPDATE lock.
|
||||
// Never compare expectedUpdatedAt in SQL: rows stamped by Postgres now() (defaultNow() on
|
||||
// insert) carry microseconds, while JS Dates are ms-truncated — SQL equality then matches
|
||||
@@ -509,7 +510,8 @@ export const resumeService = {
|
||||
|
||||
if (!resume) throw new ORPCError("NOT_FOUND");
|
||||
|
||||
return resume;
|
||||
// Clients get the current data shape (structured dates and their text in step), not the stored one.
|
||||
return { ...resume, data: parseStoredResumeData(resume.data) };
|
||||
},
|
||||
|
||||
getBySlug: async (input: {
|
||||
@@ -562,7 +564,8 @@ export const resumeService = {
|
||||
}
|
||||
}
|
||||
|
||||
return toSharedResumeResponse(redactResumeForViewer(resume, isOwner(resume, viewer)), resume.hasPassword);
|
||||
const current = { ...resume, data: parseStoredResumeData(resume.data) };
|
||||
return toSharedResumeResponse(redactResumeForViewer(current, isOwner(current, viewer)), resume.hasPassword);
|
||||
},
|
||||
|
||||
create: async (input: {
|
||||
@@ -633,7 +636,9 @@ export const resumeService = {
|
||||
|
||||
if (!existing) throw new ORPCError("NOT_FOUND");
|
||||
if (existing.isLocked) throw new ORPCError("RESUME_LOCKED");
|
||||
const normalizedData = input.data ? parseWritableResumeData(input.data) : undefined;
|
||||
const normalizedData = input.data
|
||||
? parseWritableResumeData(input.data, parseStoredResumeData(existing.data))
|
||||
: undefined;
|
||||
const updateData: Partial<typeof schema.resume.$inferSelect> = {
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
...(input.slug !== undefined ? { slug: input.slug } : {}),
|
||||
|
||||
Reference in New Issue
Block a user