mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-22 06:12:17 +10:00
refactor: ponytail audit
This commit is contained in:
@@ -14,7 +14,5 @@ export const actionsRouter = {
|
||||
})
|
||||
.input(z.object({ id: z.string() }))
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.actions.revert({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => agentService.actions.revert({ id: input.id, userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -4,10 +4,6 @@ import { storageUploadRateLimit } from "../../middleware/rate-limit";
|
||||
import { mapAgentEnvironmentError } from "./routing";
|
||||
import { agentService } from "./service";
|
||||
|
||||
function base64ToUint8Array(value: string) {
|
||||
return Uint8Array.from(Buffer.from(value, "base64"));
|
||||
}
|
||||
|
||||
export const attachmentsRouter = {
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
@@ -27,15 +23,15 @@ export const attachmentsRouter = {
|
||||
)
|
||||
.use(storageUploadRateLimit)
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.attachments.create({
|
||||
.handler(({ context, input }) =>
|
||||
agentService.attachments.create({
|
||||
userId: context.user.id,
|
||||
threadId: input.threadId,
|
||||
filename: input.filename,
|
||||
mediaType: input.mediaType,
|
||||
data: base64ToUint8Array(input.data),
|
||||
});
|
||||
}),
|
||||
data: Uint8Array.from(Buffer.from(input.data, "base64")),
|
||||
}),
|
||||
),
|
||||
|
||||
delete: protectedProcedure
|
||||
.route({
|
||||
@@ -48,7 +44,5 @@ export const attachmentsRouter = {
|
||||
.input(z.object({ id: z.string() }))
|
||||
.output(z.void())
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
await agentService.attachments.delete({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => agentService.attachments.delete({ id: input.id, userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -23,14 +23,14 @@ export const messagesRouter = {
|
||||
)
|
||||
.use(aiRequestRateLimit)
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.messages.send({
|
||||
.handler(({ context, input }) =>
|
||||
agentService.messages.send({
|
||||
userId: context.user.id,
|
||||
threadId: input.threadId,
|
||||
message: input.message,
|
||||
...(input.attachmentIds ? { attachmentIds: input.attachmentIds } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
stop: protectedProcedure
|
||||
.route({
|
||||
@@ -48,13 +48,13 @@ export const messagesRouter = {
|
||||
)
|
||||
.output(z.void())
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
await agentService.messages.stop({
|
||||
.handler(({ context, input }) =>
|
||||
agentService.messages.stop({
|
||||
userId: context.user.id,
|
||||
threadId: input.threadId,
|
||||
...(input.partialMessage ? { partialMessage: input.partialMessage } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
resume: protectedProcedure
|
||||
.route({
|
||||
@@ -66,7 +66,7 @@ export const messagesRouter = {
|
||||
})
|
||||
.input(z.object({ threadId: z.string() }))
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.messages.resume({ userId: context.user.id, threadId: input.threadId });
|
||||
}),
|
||||
.handler(({ context, input }) =>
|
||||
agentService.messages.resume({ userId: context.user.id, threadId: input.threadId }),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -332,20 +332,11 @@ function uniqueAttachmentIds(ids: unknown) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Attachment IDs must be unique." });
|
||||
}
|
||||
|
||||
if (unique.size > MAX_ATTACHMENTS_PER_MESSAGE) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Too many attachments for one message." });
|
||||
}
|
||||
|
||||
return Array.from(unique);
|
||||
}
|
||||
|
||||
function normalizeAttachmentIds(ids: unknown) {
|
||||
const unique = uniqueAttachmentIds(ids);
|
||||
return unique;
|
||||
return [...unique];
|
||||
}
|
||||
|
||||
async function getUnlinkedMessageAttachments(input: { ids: unknown; threadId: string; userId: string }) {
|
||||
const ids = normalizeAttachmentIds(input.ids);
|
||||
const ids = uniqueAttachmentIds(input.ids);
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const attachments = await db
|
||||
@@ -406,9 +397,9 @@ async function linkAttachmentsToMessage(input: {
|
||||
}
|
||||
}
|
||||
|
||||
async function readAttachmentModelInputs(attachments: AgentAttachmentRecord[]): Promise<AttachmentModelInput[]> {
|
||||
function readAttachmentModelInputs(attachments: AgentAttachmentRecord[]): Promise<AttachmentModelInput[]> {
|
||||
const storage = getStorageService();
|
||||
const inputs = await Promise.all(
|
||||
return Promise.all(
|
||||
attachments.map(async (attachment) => {
|
||||
const stored = await storage.read(attachment.storageKey);
|
||||
if (!stored) {
|
||||
@@ -418,8 +409,6 @@ async function readAttachmentModelInputs(attachments: AgentAttachmentRecord[]):
|
||||
return { attachment, data: stored.data };
|
||||
}),
|
||||
);
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function attachModelPartsToLatestUserMessage(
|
||||
@@ -643,7 +632,7 @@ function buildThreadTitle(message: UIMessage, fallback: string) {
|
||||
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
||||
}
|
||||
|
||||
async function listThreadMessages(input: { threadId: string; userId: string }) {
|
||||
function listThreadMessages(input: { threadId: string; userId: string }) {
|
||||
return db
|
||||
.select()
|
||||
.from(schema.agentMessage)
|
||||
|
||||
@@ -13,9 +13,7 @@ export const threadsRouter = {
|
||||
summary: "List agent threads",
|
||||
})
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context }) => {
|
||||
return await agentService.threads.list({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => agentService.threads.list({ userId: context.user.id })),
|
||||
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
@@ -27,14 +25,14 @@ export const threadsRouter = {
|
||||
})
|
||||
.input(z.object({ aiProviderId: z.string().optional(), sourceResumeId: z.string().optional() }))
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.threads.create({
|
||||
.handler(({ context, input }) =>
|
||||
agentService.threads.create({
|
||||
userId: context.user.id,
|
||||
locale: context.locale,
|
||||
...(input.aiProviderId ? { aiProviderId: input.aiProviderId } : {}),
|
||||
...(input.sourceResumeId ? { sourceResumeId: input.sourceResumeId } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
getOrCreateForResume: protectedProcedure
|
||||
.route({
|
||||
@@ -46,13 +44,13 @@ export const threadsRouter = {
|
||||
})
|
||||
.input(z.object({ resumeId: z.string(), aiProviderId: z.string().optional() }))
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.threads.getOrCreateForResume({
|
||||
.handler(({ context, input }) =>
|
||||
agentService.threads.getOrCreateForResume({
|
||||
userId: context.user.id,
|
||||
resumeId: input.resumeId,
|
||||
...(input.aiProviderId ? { aiProviderId: input.aiProviderId } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
get: protectedProcedure
|
||||
.route({
|
||||
@@ -64,9 +62,7 @@ export const threadsRouter = {
|
||||
})
|
||||
.input(z.object({ id: z.string() }))
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
return await agentService.threads.get({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => agentService.threads.get({ id: input.id, userId: context.user.id })),
|
||||
|
||||
archive: protectedProcedure
|
||||
.route({
|
||||
@@ -79,9 +75,7 @@ export const threadsRouter = {
|
||||
.input(z.object({ id: z.string() }))
|
||||
.output(z.void())
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
await agentService.threads.archive({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => agentService.threads.archive({ id: input.id, userId: context.user.id })),
|
||||
|
||||
delete: protectedProcedure
|
||||
.route({
|
||||
@@ -94,7 +88,5 @@ export const threadsRouter = {
|
||||
.input(z.object({ id: z.string() }))
|
||||
.output(z.void())
|
||||
.use(mapAgentEnvironmentError)
|
||||
.handler(async ({ context, input }) => {
|
||||
await agentService.threads.delete({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => agentService.threads.delete({ id: input.id, userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -7,16 +7,6 @@ import { aiRequestRateLimit } from "../../middleware/rate-limit";
|
||||
import { providerInput, updateProviderInput } from "./inputs";
|
||||
import { aiProvidersService } from "./service";
|
||||
|
||||
function isAgentEnvironmentUnavailable(error: unknown) {
|
||||
return error instanceof Error && error.message === "AGENT_ENVIRONMENT_UNAVAILABLE";
|
||||
}
|
||||
|
||||
function throwUnavailable(): never {
|
||||
throw new ORPCError("PRECONDITION_FAILED", {
|
||||
message: "AI agent workspace is unavailable because REDIS_URL or ENCRYPTION_SECRET is not configured.",
|
||||
});
|
||||
}
|
||||
|
||||
function isInvalidAiBaseUrl(error: unknown) {
|
||||
return error instanceof Error && error.message === "INVALID_AI_BASE_URL";
|
||||
}
|
||||
@@ -39,14 +29,7 @@ export const aiProvidersRouter = {
|
||||
.errors({
|
||||
PRECONDITION_FAILED: { message: "AI agent workspace is not configured.", status: 412 },
|
||||
})
|
||||
.handler(async ({ context }) => {
|
||||
try {
|
||||
return await aiProvidersService.list({ userId: context.user.id });
|
||||
} catch (error) {
|
||||
if (isAgentEnvironmentUnavailable(error)) throwUnavailable();
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
.handler(({ context }) => aiProvidersService.list({ userId: context.user.id })),
|
||||
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
@@ -74,7 +57,6 @@ export const aiProvidersRouter = {
|
||||
apiKey: input.apiKey,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAgentEnvironmentUnavailable(error)) throwUnavailable();
|
||||
if (isInvalidAiBaseUrl(error)) throwInvalidProviderConfig();
|
||||
throw error;
|
||||
}
|
||||
@@ -110,7 +92,6 @@ export const aiProvidersRouter = {
|
||||
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAgentEnvironmentUnavailable(error)) throwUnavailable();
|
||||
if (isInvalidAiBaseUrl(error)) throwInvalidProviderConfig();
|
||||
throw error;
|
||||
}
|
||||
@@ -130,14 +111,7 @@ export const aiProvidersRouter = {
|
||||
.errors({
|
||||
PRECONDITION_FAILED: { message: "AI agent workspace is not configured.", status: 412 },
|
||||
})
|
||||
.handler(async ({ context, input }) => {
|
||||
try {
|
||||
await aiProvidersService.delete({ id: input.id, userId: context.user.id });
|
||||
} catch (error) {
|
||||
if (isAgentEnvironmentUnavailable(error)) throwUnavailable();
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
.handler(({ context, input }) => aiProvidersService.delete({ id: input.id, userId: context.user.id })),
|
||||
|
||||
test: protectedProcedure
|
||||
.route({
|
||||
@@ -161,7 +135,6 @@ export const aiProvidersRouter = {
|
||||
try {
|
||||
return await aiProvidersService.test({ id: input.id, userId: context.user.id });
|
||||
} catch (error) {
|
||||
if (isAgentEnvironmentUnavailable(error)) throwUnavailable();
|
||||
if (isInvalidAiBaseUrl(error)) throwInvalidProviderConfig();
|
||||
if (error instanceof ORPCError) throw error;
|
||||
throw new ORPCError("BAD_GATEWAY", { message: "Could not reach the AI provider." });
|
||||
|
||||
@@ -86,10 +86,6 @@ function normalizeBaseUrl(input: { provider: AIProvider; baseURL?: string | null
|
||||
return resolveAiBaseUrl({ provider: input.provider, baseURL: trimmed });
|
||||
}
|
||||
|
||||
function orderByLastUsedAtDescNullsLast() {
|
||||
return desc(sql<Date>`coalesce(${schema.aiProvider.lastUsedAt}, '1970-01-01T00:00:00.000Z'::timestamptz)`);
|
||||
}
|
||||
|
||||
async function getOwnedProvider(input: { id: string; userId: string }) {
|
||||
const [provider] = await db
|
||||
.select()
|
||||
@@ -110,7 +106,10 @@ export const aiProvidersService = {
|
||||
.select()
|
||||
.from(schema.aiProvider)
|
||||
.where(eq(schema.aiProvider.userId, input.userId))
|
||||
.orderBy(orderByLastUsedAtDescNullsLast(), asc(schema.aiProvider.createdAt));
|
||||
.orderBy(
|
||||
desc(sql<Date>`coalesce(${schema.aiProvider.lastUsedAt}, '1970-01-01T00:00:00.000Z'::timestamptz)`),
|
||||
asc(schema.aiProvider.createdAt),
|
||||
);
|
||||
|
||||
return providers.map(toResponse);
|
||||
},
|
||||
@@ -250,7 +249,7 @@ export const aiProvidersService = {
|
||||
if (!updated) throw new ORPCError("NOT_FOUND");
|
||||
return toResponse(updated);
|
||||
} catch (error) {
|
||||
const [updated] = await db
|
||||
await db
|
||||
.update(schema.aiProvider)
|
||||
.set({
|
||||
enabled: false,
|
||||
@@ -258,10 +257,8 @@ export const aiProvidersService = {
|
||||
testError: error instanceof Error ? error.message : "Failed to test provider.",
|
||||
lastTestedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(schema.aiProvider.id, input.id), eq(schema.aiProvider.userId, input.userId)))
|
||||
.returning();
|
||||
.where(and(eq(schema.aiProvider.id, input.id), eq(schema.aiProvider.userId, input.userId)));
|
||||
|
||||
if (!updated) throw error;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -80,9 +80,7 @@ export function supportsOpenAIWebSearch(model: string) {
|
||||
|
||||
if (OPENAI_WEB_SEARCH_RESPONSES_MODEL_IDS.has(normalized)) return true;
|
||||
|
||||
return Array.from(OPENAI_WEB_SEARCH_RESPONSES_MODEL_IDS).some((modelId) =>
|
||||
isDateSnapshotForModel(normalized, modelId),
|
||||
);
|
||||
return [...OPENAI_WEB_SEARCH_RESPONSES_MODEL_IDS].some((modelId) => isDateSnapshotForModel(normalized, modelId));
|
||||
}
|
||||
|
||||
export function supportsProviderNativeWebSearch(provider: AiProviderCapabilityInput) {
|
||||
|
||||
@@ -15,7 +15,7 @@ afterEach(() => {
|
||||
function stubOpenAICompatibleResponse(response?: { content?: string; finishReason?: string }) {
|
||||
let requestBody: unknown;
|
||||
|
||||
const fetchMock = vi.fn(async (_input: unknown, init?: { body?: unknown }) => {
|
||||
const fetchMock = vi.fn((_input: unknown, init?: { body?: unknown }) => {
|
||||
const body = JSON.parse(String(init?.body ?? "{}")) as { max_tokens?: number };
|
||||
requestBody = body;
|
||||
const hasEnoughOutputTokens = (body.max_tokens ?? 0) >= 128;
|
||||
|
||||
@@ -361,7 +361,7 @@ async function chat(input: ChatInput) {
|
||||
"Return one or more cohesive resume change proposals. Each proposal must include a title, optional summary, and valid JSON Patch operations against the current resume data. The tool validates but does not apply changes.",
|
||||
inputSchema: resumePatchProposalToolInputSchema,
|
||||
outputSchema: resumePatchProposalToolOutputSchema,
|
||||
execute: async (toolInput) => {
|
||||
execute: (toolInput) => {
|
||||
const proposals = normalizeResumePatchProposals(toolInput, input.resumeUpdatedAt);
|
||||
|
||||
for (const proposal of proposals) {
|
||||
|
||||
@@ -348,7 +348,7 @@ async function fetchLinkedInJobPostingText(jobId: string): Promise<string> {
|
||||
// Best-effort fetch + strip of a job posting page. http(s) only, size/time capped.
|
||||
export async function fetchJobPostingText(url: string): Promise<string> {
|
||||
const jobId = linkedInJobId(url);
|
||||
if (jobId) return await fetchLinkedInJobPostingText(jobId);
|
||||
if (jobId) return fetchLinkedInJobPostingText(jobId);
|
||||
if (isLinkedInUrl(url)) {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "The LinkedIn job URL must include a job posting ID." });
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ export const crudRouter = {
|
||||
})
|
||||
.input(applicationDto.list.input)
|
||||
.output(applicationDto.list.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.list({
|
||||
.handler(({ input, context }) =>
|
||||
applicationService.list({
|
||||
userId: context.user.id,
|
||||
...(input.status ? { status: input.status } : {}),
|
||||
...(input.tags ? { tags: input.tags } : {}),
|
||||
includeArchived: input.includeArchived,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
getById: protectedProcedure
|
||||
.route({
|
||||
@@ -39,9 +39,7 @@ export const crudRouter = {
|
||||
})
|
||||
.input(applicationDto.getById.input)
|
||||
.output(applicationDto.getById.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.getById({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.getById({ id: input.id, userId: context.user.id })),
|
||||
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
@@ -57,9 +55,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.create.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.create.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.create({ userId: context.user.id, ...input });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.create({ userId: context.user.id, ...input })),
|
||||
|
||||
import: protectedProcedure
|
||||
.route({
|
||||
@@ -75,9 +71,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.import.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.import.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.importMany({ userId: context.user.id, items: input.items });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.importMany({ userId: context.user.id, items: input.items })),
|
||||
|
||||
update: protectedProcedure
|
||||
.route({
|
||||
@@ -93,9 +87,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.update.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.update.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.update({ userId: context.user.id, ...input });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.update({ userId: context.user.id, ...input })),
|
||||
|
||||
attachDocument: protectedProcedure
|
||||
.route({
|
||||
@@ -153,9 +145,9 @@ export const crudRouter = {
|
||||
.input(applicationDto.removeDocument.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.removeDocument.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.removeDocument({ id: input.id, userId: context.user.id, kind: input.kind });
|
||||
}),
|
||||
.handler(({ input, context }) =>
|
||||
applicationService.removeDocument({ id: input.id, userId: context.user.id, kind: input.kind }),
|
||||
),
|
||||
|
||||
addNote: protectedProcedure
|
||||
.route({
|
||||
@@ -170,9 +162,14 @@ export const crudRouter = {
|
||||
.input(applicationDto.addNote.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.addNote.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.addNote({ id: input.id, userId: context.user.id, text: input.text, date: input.date });
|
||||
}),
|
||||
.handler(({ input, context }) =>
|
||||
applicationService.addNote({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
text: input.text,
|
||||
date: input.date,
|
||||
}),
|
||||
),
|
||||
|
||||
updateTimelineEntry: protectedProcedure
|
||||
.route({
|
||||
@@ -187,9 +184,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.updateTimelineEntry.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.updateTimelineEntry.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.updateTimelineEntry({ ...input, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.updateTimelineEntry({ ...input, userId: context.user.id })),
|
||||
|
||||
deleteTimelineEntry: protectedProcedure
|
||||
.route({
|
||||
@@ -205,9 +200,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.deleteTimelineEntry.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.deleteTimelineEntry.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.deleteTimelineEntry({ ...input, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.deleteTimelineEntry({ ...input, userId: context.user.id })),
|
||||
|
||||
delete: protectedProcedure
|
||||
.route({
|
||||
@@ -222,9 +215,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.delete.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.delete.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.delete({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.delete({ id: input.id, userId: context.user.id })),
|
||||
|
||||
bulkUpdate: protectedProcedure
|
||||
.route({
|
||||
@@ -240,9 +231,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.bulkUpdate.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.bulkUpdate.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.bulkUpdate({ userId: context.user.id, ...input });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.bulkUpdate({ userId: context.user.id, ...input })),
|
||||
|
||||
bulkDelete: protectedProcedure
|
||||
.route({
|
||||
@@ -257,9 +246,7 @@ export const crudRouter = {
|
||||
.input(applicationDto.bulkDelete.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(applicationDto.bulkDelete.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return applicationService.bulkDelete({ userId: context.user.id, ids: input.ids });
|
||||
}),
|
||||
.handler(({ input, context }) => applicationService.bulkDelete({ userId: context.user.id, ids: input.ids })),
|
||||
|
||||
stats: protectedProcedure
|
||||
.route({
|
||||
@@ -273,9 +260,7 @@ export const crudRouter = {
|
||||
})
|
||||
.input(applicationDto.stats.input)
|
||||
.output(applicationDto.stats.output)
|
||||
.handler(async ({ context }) => {
|
||||
return applicationService.stats({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => applicationService.stats({ userId: context.user.id })),
|
||||
|
||||
tags: protectedProcedure
|
||||
.route({
|
||||
@@ -288,7 +273,5 @@ export const crudRouter = {
|
||||
successDescription: "Distinct tags.",
|
||||
})
|
||||
.output(applicationDto.tags.output)
|
||||
.handler(async ({ context }) => {
|
||||
return applicationService.listTags({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => applicationService.listTags({ userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -47,16 +47,12 @@ function noteEntry(text: string, date?: string): ApplicationTimelineEntry {
|
||||
return { id: generateId(), type: "note", text, at: date ? atFromDateString(date) : new Date() };
|
||||
}
|
||||
|
||||
function byNewest(a: ApplicationTimelineEntry, b: ApplicationTimelineEntry) {
|
||||
return new Date(b.at).getTime() - new Date(a.at).getTime();
|
||||
}
|
||||
|
||||
function timelineDay(value: Date | string) {
|
||||
return timelineDate(value).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function sortTimeline(activity: ApplicationTimelineEntry[]): ApplicationTimelineEntry[] {
|
||||
return [...activity].sort(byNewest);
|
||||
return [...activity].sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());
|
||||
}
|
||||
|
||||
function currentStageAnchor(activity: ApplicationTimelineEntry[], status: ApplicationStatus) {
|
||||
@@ -423,7 +419,7 @@ export const applicationService = {
|
||||
return stripUserId(updated);
|
||||
},
|
||||
|
||||
updateTimelineEntry: async (input: {
|
||||
updateTimelineEntry: (input: {
|
||||
id: string;
|
||||
userId: string;
|
||||
entryId: string;
|
||||
@@ -474,7 +470,7 @@ export const applicationService = {
|
||||
});
|
||||
},
|
||||
|
||||
deleteTimelineEntry: async (input: { id: string; userId: string; entryId: string }) => {
|
||||
deleteTimelineEntry: (input: { id: string; userId: string; entryId: string }) => {
|
||||
return db.transaction(async (tx) => {
|
||||
await tx.execute(sql`
|
||||
select 1 from ${schema.application}
|
||||
|
||||
@@ -15,9 +15,7 @@ export const authRouter = {
|
||||
"Returns a list of all authentication providers enabled on this Reactive Resume instance, along with their display names. Possible providers include password-based credentials, Google, GitHub, LinkedIn, and custom OAuth. No authentication required.",
|
||||
successDescription: "A map of enabled authentication provider identifiers to their display names.",
|
||||
})
|
||||
.handler((): ProviderList => {
|
||||
return authService.providers.list();
|
||||
}),
|
||||
.handler((): ProviderList => authService.providers.list()),
|
||||
},
|
||||
|
||||
exportData: protectedProcedure
|
||||
@@ -31,9 +29,7 @@ export const authRouter = {
|
||||
"Returns a JSON-serializable export of the authenticated user's data, including their public profile fields and all of their resumes. Secrets such as password hashes, tokens, and API keys are never included. Requires authentication.",
|
||||
successDescription: "The user's exported account data.",
|
||||
})
|
||||
.handler(async ({ context }) => {
|
||||
return await authService.exportData({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => authService.exportData({ userId: context.user.id })),
|
||||
|
||||
deleteAccount: protectedProcedure
|
||||
.route({
|
||||
@@ -46,7 +42,5 @@ export const authRouter = {
|
||||
"Permanently deletes the authenticated user's account, including all resumes, uploaded files (profile pictures, screenshots, PDFs), and associated data. This action is irreversible. Requires authentication.",
|
||||
successDescription: "The user account and all associated data have been successfully deleted.",
|
||||
})
|
||||
.handler(async ({ context }): Promise<void> => {
|
||||
return await authService.deleteAccount({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => authService.deleteAccount({ userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -17,7 +17,5 @@ export const analysisRouter = {
|
||||
})
|
||||
.input(z.object({ id: z.string().describe("The unique identifier of the resume.") }))
|
||||
.output(storedResumeAnalysisSchema.nullable())
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.analysis.getById({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => resumeService.analysis.getById({ id: input.id, userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -19,13 +19,13 @@ export const crudRouter = {
|
||||
})
|
||||
.input(resumeDto.list.input.optional().default({ tags: [], sort: "lastUpdatedAt" }))
|
||||
.output(resumeDto.list.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return resumeService.list({
|
||||
.handler(({ input, context }) =>
|
||||
resumeService.list({
|
||||
userId: context.user.id,
|
||||
tags: input.tags,
|
||||
sort: input.sort,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
getById: protectedProcedure
|
||||
.route({
|
||||
@@ -40,9 +40,7 @@ export const crudRouter = {
|
||||
})
|
||||
.input(resumeDto.getById.input)
|
||||
.output(resumeDto.getById.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.getById({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => resumeService.getById({ id: input.id, userId: context.user.id })),
|
||||
|
||||
create: protectedProcedure
|
||||
.route({
|
||||
@@ -64,16 +62,16 @@ export const crudRouter = {
|
||||
status: 400,
|
||||
},
|
||||
})
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.create({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.create({
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
tags: input.tags,
|
||||
locale: context.locale,
|
||||
userId: context.user.id,
|
||||
...(input.withSampleData ? { data: createSampleResumeData(input.name) } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
import: protectedProcedure
|
||||
.route({
|
||||
@@ -139,8 +137,8 @@ export const crudRouter = {
|
||||
status: 400,
|
||||
},
|
||||
})
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.update({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.update({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
...(input.name !== undefined ? { name: input.name } : {}),
|
||||
@@ -148,8 +146,8 @@ export const crudRouter = {
|
||||
...(input.tags !== undefined ? { tags: input.tags } : {}),
|
||||
...(input.data !== undefined ? { data: input.data } : {}),
|
||||
...(input.isPublic !== undefined ? { isPublic: input.isPublic } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
patch: protectedProcedure
|
||||
.route({
|
||||
@@ -175,14 +173,14 @@ export const crudRouter = {
|
||||
status: 409,
|
||||
},
|
||||
})
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.patch({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.patch({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
operations: input.operations,
|
||||
...(input.expectedUpdatedAt ? { expectedUpdatedAt: input.expectedUpdatedAt } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
setLocked: protectedProcedure
|
||||
.route({
|
||||
@@ -198,13 +196,13 @@ export const crudRouter = {
|
||||
.input(resumeDto.setLocked.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(resumeDto.setLocked.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.setLocked({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.setLocked({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
isLocked: input.isLocked,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
duplicate: protectedProcedure
|
||||
.route({
|
||||
@@ -247,7 +245,5 @@ export const crudRouter = {
|
||||
.input(resumeDto.delete.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(resumeDto.delete.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.delete({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => resumeService.delete({ id: input.id, userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -75,10 +75,10 @@ export const downloadResumePdfProcedure = protectedProcedure
|
||||
}),
|
||||
)
|
||||
.use(pdfExportRateLimit)
|
||||
.handler(async ({ context, input }) => {
|
||||
return createResumePdfDownload({
|
||||
.handler(({ context, input }) =>
|
||||
createResumePdfDownload({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
...(input.target ? { target: input.target } : {}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -165,10 +165,7 @@ const tags = {
|
||||
.from(schema.resume)
|
||||
.where(eq(schema.resume.userId, input.userId));
|
||||
|
||||
const uniqueTags = new Set(result.flatMap((tag) => tag.tags));
|
||||
const sortedTags = Array.from(uniqueTags).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return sortedTags;
|
||||
return [...new Set(result.flatMap((tag) => tag.tags))].sort((a, b) => a.localeCompare(b));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -422,8 +419,8 @@ export const resumeService = {
|
||||
},
|
||||
},
|
||||
|
||||
list: async (input: { userId: string; tags: string[]; sort: "lastUpdatedAt" | "createdAt" | "name" }) => {
|
||||
return await db
|
||||
list: (input: { userId: string; tags: string[]; sort: "lastUpdatedAt" | "createdAt" | "name" }) =>
|
||||
db
|
||||
.select({
|
||||
id: schema.resume.id,
|
||||
name: schema.resume.name,
|
||||
@@ -449,8 +446,7 @@ export const resumeService = {
|
||||
.with("createdAt", () => asc(schema.resume.createdAt))
|
||||
.with("name", () => asc(schema.resume.name))
|
||||
.exhaustive(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
getById: async (input: { id: string; userId: string }) => {
|
||||
const [resume] = await db
|
||||
|
||||
@@ -18,13 +18,13 @@ export const sharingRouter = {
|
||||
})
|
||||
.input(resumeDto.getBySlug.input)
|
||||
.output(resumeDto.getBySlug.output)
|
||||
.handler(async ({ input, context }) => {
|
||||
return resumeService.getBySlug({
|
||||
.handler(({ input, context }) =>
|
||||
resumeService.getBySlug({
|
||||
...input,
|
||||
requestHeaders: context.reqHeaders,
|
||||
...(context.user?.id ? { currentUserId: context.user.id } : {}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
setPassword: protectedProcedure
|
||||
.route({
|
||||
@@ -40,13 +40,13 @@ export const sharingRouter = {
|
||||
.input(resumeDto.setPassword.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(resumeDto.setPassword.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.setPassword({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.setPassword({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
password: input.password,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
||||
verifyPassword: publicProcedure
|
||||
.route({
|
||||
@@ -68,14 +68,15 @@ export const sharingRouter = {
|
||||
)
|
||||
.use(resumePasswordRateLimit)
|
||||
.output(z.boolean())
|
||||
.handler(async ({ context, input }): Promise<boolean> => {
|
||||
return resumeService.verifyPassword({
|
||||
username: input.username,
|
||||
slug: input.slug,
|
||||
password: input.password,
|
||||
...(context.resHeaders ? { responseHeaders: context.resHeaders } : {}),
|
||||
});
|
||||
}),
|
||||
.handler(
|
||||
({ context, input }): Promise<boolean> =>
|
||||
resumeService.verifyPassword({
|
||||
username: input.username,
|
||||
slug: input.slug,
|
||||
password: input.password,
|
||||
...(context.resHeaders ? { responseHeaders: context.resHeaders } : {}),
|
||||
}),
|
||||
),
|
||||
|
||||
removePassword: protectedProcedure
|
||||
.route({
|
||||
@@ -91,10 +92,10 @@ export const sharingRouter = {
|
||||
.input(resumeDto.removePassword.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(resumeDto.removePassword.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.removePassword({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.removePassword({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -24,9 +24,7 @@ export const resumeStatisticsRouter = {
|
||||
lastDownloadedAt: z.date().nullable().describe("Timestamp of the last download, or null if never downloaded."),
|
||||
}),
|
||||
)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.statistics.getById({ id: input.id, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) => resumeService.statistics.getById({ id: input.id, userId: context.user.id })),
|
||||
|
||||
getDailyById: protectedProcedure
|
||||
.route({
|
||||
@@ -54,7 +52,11 @@ export const resumeStatisticsRouter = {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.statistics.getDailySeries({ id: input.id, userId: context.user.id, days: input.days });
|
||||
}),
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.statistics.getDailySeries({
|
||||
id: input.id,
|
||||
userId: context.user.id,
|
||||
days: input.days,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -15,7 +15,5 @@ export const tagsRouter = {
|
||||
successDescription: "A sorted array of unique tag strings.",
|
||||
})
|
||||
.output(z.array(z.string()))
|
||||
.handler(async ({ context }) => {
|
||||
return resumeService.tags.list({ userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context }) => resumeService.tags.list({ userId: context.user.id })),
|
||||
};
|
||||
|
||||
@@ -17,9 +17,9 @@ export const versionsRouter = {
|
||||
})
|
||||
.input(resumeDto.listVersions.input)
|
||||
.output(resumeDto.listVersions.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.versions.list({ resumeId: input.resumeId, userId: context.user.id });
|
||||
}),
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.versions.list({ resumeId: input.resumeId, userId: context.user.id }),
|
||||
),
|
||||
|
||||
restoreVersion: protectedProcedure
|
||||
.route({
|
||||
@@ -35,11 +35,11 @@ export const versionsRouter = {
|
||||
.input(resumeDto.restoreVersion.input)
|
||||
.use(resumeMutationRateLimit)
|
||||
.output(resumeDto.restoreVersion.output)
|
||||
.handler(async ({ context, input }) => {
|
||||
return resumeService.versions.restore({
|
||||
.handler(({ context, input }) =>
|
||||
resumeService.versions.restore({
|
||||
resumeId: input.resumeId,
|
||||
versionId: input.versionId,
|
||||
userId: context.user.id,
|
||||
});
|
||||
}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -15,9 +15,7 @@ const userRouter = {
|
||||
successDescription: "The total number of registered users.",
|
||||
})
|
||||
.output(z.number().describe("The total number of registered users."))
|
||||
.handler(async (): Promise<number> => {
|
||||
return await statisticsService.user.getCount();
|
||||
}),
|
||||
.handler(() => statisticsService.user.getCount()),
|
||||
};
|
||||
|
||||
const resumeRouter = {
|
||||
@@ -33,9 +31,7 @@ const resumeRouter = {
|
||||
successDescription: "The total number of resumes created.",
|
||||
})
|
||||
.output(z.number().describe("The total number of resumes created."))
|
||||
.handler(async (): Promise<number> => {
|
||||
return await statisticsService.resume.getCount();
|
||||
}),
|
||||
.handler(() => statisticsService.resume.getCount()),
|
||||
};
|
||||
|
||||
const githubRouter = {
|
||||
@@ -51,9 +47,7 @@ const githubRouter = {
|
||||
successDescription: "The number of GitHub stars for the Reactive Resume repository.",
|
||||
})
|
||||
.output(z.number().describe("The number of GitHub stars."))
|
||||
.handler(async (): Promise<number> => {
|
||||
return await statisticsService.github.getStarCount();
|
||||
}),
|
||||
.handler(() => statisticsService.github.getStarCount()),
|
||||
};
|
||||
|
||||
export const statisticsRouter = {
|
||||
|
||||
@@ -321,20 +321,13 @@ class S3StorageService implements StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
function createStorageService(): StorageService {
|
||||
if (env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY && env.S3_BUCKET) {
|
||||
return new S3StorageService();
|
||||
}
|
||||
|
||||
return new LocalStorageService();
|
||||
}
|
||||
|
||||
let cachedService: StorageService | null = null;
|
||||
|
||||
export function getStorageService(): StorageService {
|
||||
if (cachedService) return cachedService;
|
||||
|
||||
cachedService = createStorageService();
|
||||
cachedService ??=
|
||||
env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY && env.S3_BUCKET
|
||||
? new S3StorageService()
|
||||
: new LocalStorageService();
|
||||
return cachedService;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user