feat: complete TASK-WP2-02 project autosave
This commit is contained in:
@@ -15,6 +15,9 @@ import {
|
||||
AdminLoginSendRequestSchema,
|
||||
AdminSessionResponseSchema,
|
||||
BootstrapResponseSchema,
|
||||
CanvasBackgroundAdjustmentsSchema,
|
||||
CanvasElementSchema,
|
||||
CanvasStateSchema,
|
||||
CorrelationIdSchema,
|
||||
AuthenticatedUserSchema,
|
||||
CreditSummarySchema,
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
FailedEmptyTrashResponseSchema,
|
||||
GenerationProjectItemSchema,
|
||||
ProjectDetailResponseSchema,
|
||||
ProjectEditableStateSchema,
|
||||
ProjectIdSchema,
|
||||
ProjectImageItemSchema,
|
||||
ProjectListQuerySchema,
|
||||
@@ -41,6 +45,9 @@ import {
|
||||
ProjectRatioSchema,
|
||||
ProjectRenameRequestSchema,
|
||||
ProjectRenameResponseSchema,
|
||||
ProjectStateConflictResponseSchema,
|
||||
ProjectStateSaveHeadersSchema,
|
||||
ProjectStateSaveResponseSchema,
|
||||
ProjectSummarySchema,
|
||||
ProjectViewStatusSchema,
|
||||
RegistrationCompleteHeadersSchema,
|
||||
@@ -66,6 +73,8 @@ import {
|
||||
type ProjectListQuery,
|
||||
type ProjectParams,
|
||||
type ProjectRenameRequest,
|
||||
type ProjectEditableState,
|
||||
type ProjectStateSaveHeaders,
|
||||
type RegistrationCompleteRequest,
|
||||
type RegistrationSendRequest,
|
||||
} from "@dada/shared-contracts";
|
||||
@@ -196,6 +205,9 @@ function projectFailure(reply: FastifyReply, correlationId: string, error: unkno
|
||||
project_not_found: 404,
|
||||
project_ratio_fixed: 409,
|
||||
project_retry_not_allowed: 409,
|
||||
project_state_conflict: 412,
|
||||
project_state_idempotency_conflict: 409,
|
||||
project_state_invalid: 400,
|
||||
} as const;
|
||||
return reply.code(mapping[error.code]).send(null);
|
||||
}
|
||||
@@ -221,6 +233,7 @@ function projectSummaryResponse(project: ProjectSummaryView) {
|
||||
function projectDetailResponse(project: ProjectDetailView) {
|
||||
return {
|
||||
...projectSummaryResponse(project),
|
||||
canvas_state: project.canvasState,
|
||||
created_at: project.createdAt,
|
||||
draft_prompt: project.draftPrompt,
|
||||
generations: project.generations.map((generation) => ({
|
||||
@@ -239,6 +252,7 @@ function projectDetailResponse(project: ProjectDetailView) {
|
||||
})),
|
||||
pixel_height: project.pixelHeight,
|
||||
pixel_width: project.pixelWidth,
|
||||
save_status: project.saveStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,6 +350,9 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
BrowserSupportRequestSchema,
|
||||
BrowserSupportSuccessSchema,
|
||||
BootstrapResponseSchema,
|
||||
CanvasBackgroundAdjustmentsSchema,
|
||||
CanvasElementSchema,
|
||||
CanvasStateSchema,
|
||||
StateSseEventSchema,
|
||||
ModelConfigSseEventSchema,
|
||||
ModelRuntimeSseEventSchema,
|
||||
@@ -350,8 +367,12 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
GenerationProjectItemSchema,
|
||||
ProjectImageItemSchema,
|
||||
ProjectDetailResponseSchema,
|
||||
ProjectEditableStateSchema,
|
||||
ProjectRenameRequestSchema,
|
||||
ProjectRenameResponseSchema,
|
||||
ProjectStateSaveHeadersSchema,
|
||||
ProjectStateSaveResponseSchema,
|
||||
ProjectStateConflictResponseSchema,
|
||||
FailedEmptyTrashRequestSchema,
|
||||
FailedEmptyTrashResponseSchema,
|
||||
]) {
|
||||
@@ -1084,6 +1105,62 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
"/api/v1/projects/:projectId/state",
|
||||
{
|
||||
attachValidation: true,
|
||||
schema: {
|
||||
body: Type.Ref(ProjectEditableStateSchema),
|
||||
headers: Type.Ref(ProjectStateSaveHeadersSchema),
|
||||
operationId: "saveProjectState",
|
||||
params: Type.Ref(ProjectParamsSchema),
|
||||
response: {
|
||||
200: Type.Ref(ProjectStateSaveResponseSchema),
|
||||
400: Type.Null(),
|
||||
401: Type.Ref(ErrorEnvelopeSchema),
|
||||
403: Type.Ref(ErrorEnvelopeSchema),
|
||||
404: Type.Null(),
|
||||
409: Type.Ref(ErrorEnvelopeSchema),
|
||||
412: Type.Ref(ProjectStateConflictResponseSchema),
|
||||
503: Type.Ref(ErrorEnvelopeSchema),
|
||||
},
|
||||
tags: ["Projects"],
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
if (request.validationError) return reply.code(400).send(null);
|
||||
if (!options.registration || !options.projects) {
|
||||
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
|
||||
}
|
||||
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
|
||||
const headers = request.headers as ProjectStateSaveHeaders;
|
||||
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
|
||||
try {
|
||||
const owner = options.registration.authorizeUserMutation({ csrfToken: headers["x-csrf-token"], sessionToken: token });
|
||||
const saved = options.projects.saveProjectState({
|
||||
expectedStateVersion: Number(headers["if-match"]),
|
||||
idempotencyKey: headers["idempotency-key"],
|
||||
ownerId: owner.userId,
|
||||
projectId: (request.params as ProjectParams).projectId,
|
||||
state: request.body as ProjectEditableState,
|
||||
});
|
||||
return { save_status: "saved" as const, state_version: saved.stateVersion };
|
||||
} catch (error) {
|
||||
if (error instanceof RegistrationError) return registrationFailure(reply, request.id, error);
|
||||
if (error instanceof ProjectError && error.code === "project_state_conflict") {
|
||||
return reply.code(412).send({
|
||||
latest_state_version: error.latestStateVersion ?? 1,
|
||||
save_status: "conflicted" as const,
|
||||
});
|
||||
}
|
||||
if (error instanceof ProjectError && error.code === "project_state_idempotency_conflict") {
|
||||
return reply.code(409).send(createErrorEnvelope({ code: "IDEMPOTENCY_KEY_CONFLICT", correlationId: request.id }));
|
||||
}
|
||||
return projectFailure(reply, request.id, error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/api/v1/projects/:projectId",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user