feat: complete TASK-WP2-02 project autosave
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { Type, type Static } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
|
||||
const uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$";
|
||||
const isoTimestampPattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$";
|
||||
const stableResourcePattern = "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$";
|
||||
|
||||
export const CanvasBackgroundAdjustmentsSchema = Type.Object(
|
||||
{
|
||||
brightness: Type.Number({ maximum: 100, minimum: -100 }),
|
||||
contrast: Type.Number({ maximum: 100, minimum: -100 }),
|
||||
crop: Type.Union([
|
||||
Type.Object({
|
||||
height: Type.Number({ maximum: 1, minimum: 0 }),
|
||||
width: Type.Number({ maximum: 1, minimum: 0 }),
|
||||
x: Type.Number({ maximum: 1, minimum: 0 }),
|
||||
y: Type.Number({ maximum: 1, minimum: 0 }),
|
||||
}, { additionalProperties: false }),
|
||||
Type.Null(),
|
||||
]),
|
||||
filter: Type.String({ maxLength: 64, pattern: "^[A-Za-z0-9_-]+$" }),
|
||||
fit: Type.Union([Type.Literal("fill"), Type.Literal("fit"), Type.Literal("crop")]),
|
||||
saturation: Type.Number({ maximum: 100, minimum: -100 }),
|
||||
sharpness: Type.Number({ maximum: 100, minimum: 0 }),
|
||||
temperature: Type.Number({ maximum: 100, minimum: -100 }),
|
||||
},
|
||||
{ additionalProperties: false, $id: "CanvasBackgroundAdjustments" },
|
||||
);
|
||||
|
||||
const CanvasScalarSchema = Type.Union([
|
||||
Type.String({ maxLength: 4_000 }), Type.Number(), Type.Boolean(), Type.Null(),
|
||||
]);
|
||||
|
||||
export const CanvasElementSchema = Type.Object(
|
||||
{
|
||||
colors: Type.Optional(Type.Array(Type.String({ pattern: "^#[0-9A-Fa-f]{6}$" }), { maxItems: 5, minItems: 5 })),
|
||||
content: Type.Optional(Type.String({ maxLength: 4_000 })),
|
||||
coordinates: Type.Optional(Type.Object({
|
||||
latitude: Type.Number({ maximum: 90, minimum: -90 }),
|
||||
longitude: Type.Number({ maximum: 180, minimum: -180 }),
|
||||
}, { additionalProperties: false })),
|
||||
created_at: Type.String({ pattern: isoTimestampPattern }),
|
||||
dynamic_fields: Type.Optional(Type.Record(Type.String({ pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$" }), CanvasScalarSchema)),
|
||||
element_id: Type.String({ pattern: uuidPattern }),
|
||||
font_override: Type.Optional(Type.String({ maxLength: 120, pattern: stableResourcePattern })),
|
||||
font_size: Type.Optional(Type.Number({ maximum: 512, minimum: 1 })),
|
||||
formatted_value: Type.Optional(Type.String({ maxLength: 4_000 })),
|
||||
opacity: Type.Number({ maximum: 1, minimum: 0 }),
|
||||
position: Type.Object({ x: Type.Number(), y: Type.Number() }, { additionalProperties: false }),
|
||||
resource_version: Type.String({ maxLength: 120, pattern: stableResourcePattern }),
|
||||
rotation: Type.Number({ maximum: 360, minimum: -360 }),
|
||||
scale: Type.Object({
|
||||
x: Type.Number({ maximum: 100, minimum: 0.01 }),
|
||||
y: Type.Number({ maximum: 100, minimum: 0.01 }),
|
||||
}, { additionalProperties: false }),
|
||||
style_id: Type.Optional(Type.String({ maxLength: 120, pattern: stableResourcePattern })),
|
||||
style_parameters: Type.Optional(Type.Record(Type.String({ pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$" }), CanvasScalarSchema)),
|
||||
template_or_asset_id: Type.String({ maxLength: 120, pattern: stableResourcePattern }),
|
||||
type: Type.Union([
|
||||
Type.Literal("text_template"), Type.Literal("static_sticker"),
|
||||
Type.Literal("color_card"), Type.Literal("dynamic_sticker"),
|
||||
]),
|
||||
z_index: Type.Integer({ maximum: 49, minimum: 0 }),
|
||||
},
|
||||
{ additionalProperties: false, $id: "CanvasElement" },
|
||||
);
|
||||
|
||||
export const CanvasStateSchema = Type.Object(
|
||||
{
|
||||
background: Type.Object({
|
||||
adjustments: Type.Ref(CanvasBackgroundAdjustmentsSchema),
|
||||
asset_id: Type.Union([Type.String({ pattern: uuidPattern }), Type.Null()]),
|
||||
}, { additionalProperties: false }),
|
||||
elements: Type.Array(Type.Ref(CanvasElementSchema), { maxItems: 50 }),
|
||||
pixel_height: Type.Integer({ minimum: 1 }),
|
||||
pixel_width: Type.Integer({ minimum: 1 }),
|
||||
ratio: Type.Union([Type.Literal("3:4"), Type.Literal("1:1"), Type.Literal("4:3"), Type.Literal("9:16")]),
|
||||
schema_version: Type.Literal(1),
|
||||
},
|
||||
{ additionalProperties: false, $id: "CanvasState" },
|
||||
);
|
||||
|
||||
export const ProjectEditableStateSchema = Type.Object(
|
||||
{
|
||||
canvas_state: Type.Ref(CanvasStateSchema),
|
||||
name: Type.String({ maxLength: 80, minLength: 1 }),
|
||||
},
|
||||
{ additionalProperties: false, $id: "ProjectEditableState" },
|
||||
);
|
||||
|
||||
export const ProjectStateSaveHeadersSchema = Type.Object(
|
||||
{
|
||||
"idempotency-key": Type.String({ maxLength: 200, minLength: 32, pattern: "^[A-Za-z0-9_-]+$" }),
|
||||
"if-match": Type.String({ pattern: "^[1-9][0-9]*$" }),
|
||||
"x-csrf-token": Type.String({ maxLength: 512, minLength: 32 }),
|
||||
},
|
||||
{ additionalProperties: true, $id: "ProjectStateSaveHeaders" },
|
||||
);
|
||||
|
||||
export const ProjectStateSaveResponseSchema = Type.Object(
|
||||
{ save_status: Type.Literal("saved"), state_version: Type.Integer({ minimum: 2 }) },
|
||||
{ additionalProperties: false, $id: "ProjectStateSaveResponse" },
|
||||
);
|
||||
|
||||
export const ProjectStateConflictResponseSchema = Type.Object(
|
||||
{ latest_state_version: Type.Integer({ minimum: 1 }), save_status: Type.Literal("conflicted") },
|
||||
{ additionalProperties: false, $id: "ProjectStateConflictResponse" },
|
||||
);
|
||||
|
||||
export type CanvasState = Static<typeof CanvasStateSchema>;
|
||||
export type ProjectEditableState = Static<typeof ProjectEditableStateSchema>;
|
||||
export type ProjectStateSaveHeaders = Static<typeof ProjectStateSaveHeadersSchema>;
|
||||
|
||||
export function isCanvasState(value: unknown): value is CanvasState {
|
||||
if (!Value.Check(CanvasStateSchema, [CanvasBackgroundAdjustmentsSchema, CanvasElementSchema], value)) return false;
|
||||
const state = value as CanvasState;
|
||||
const ids = new Set(state.elements.map((element) => element.element_id));
|
||||
const zIndexes = new Set(state.elements.map((element) => element.z_index));
|
||||
return ids.size === state.elements.length && zIndexes.size === state.elements.length;
|
||||
}
|
||||
|
||||
export function isProjectEditableState(value: unknown): value is ProjectEditableState {
|
||||
return Value.Check(
|
||||
ProjectEditableStateSchema,
|
||||
[CanvasBackgroundAdjustmentsSchema, CanvasElementSchema, CanvasStateSchema],
|
||||
value,
|
||||
)
|
||||
&& isCanvasState((value as ProjectEditableState).canvas_state);
|
||||
}
|
||||
Reference in New Issue
Block a user