Refactor API types and sync logic
Rename and refactor the types passed over the API to be based on an entity that's either living or a tombstone. A living entity has a deleted property that's either undefined or false, while a tombstone has a deleted property set to true. All entities have a numeric id and an updatedAt timestamp. To sync entities, an array of replacements are passed around. Living entities are replaced with tombstones when they're deleted. And tombstones are replaced with living entities when restored.
This commit is contained in:
parent
251e83f640
commit
fe06d0d6bd
36 changed files with 1242 additions and 834 deletions
|
@ -1,41 +1,8 @@
|
|||
import type { SchedulePatch } from "~/shared/types/schedule";
|
||||
import { z } from "zod/v4-mini";
|
||||
import { readAccounts, readSchedule, writeSchedule } from "~/server/database";
|
||||
import { broadcastUpdate } from "~/server/streams";
|
||||
import { applyChangeArray } from "~/shared/utils/changes";
|
||||
|
||||
function isChange(change: unknown) {
|
||||
return (
|
||||
typeof change === "object"
|
||||
&& change !== null
|
||||
&& "op" in change
|
||||
&& (
|
||||
change.op === "set" || change.op === "del"
|
||||
)
|
||||
&& "data" in change
|
||||
&& typeof change.data === "object"
|
||||
&& change.data !== null
|
||||
&& "id" in change.data
|
||||
&& typeof change.data.id === "string"
|
||||
)
|
||||
}
|
||||
|
||||
function isChangeArray(data: unknown) {
|
||||
return data instanceof Array && data.every(item => isChange(item));
|
||||
}
|
||||
|
||||
function isPatch(data: unknown): SchedulePatch {
|
||||
if (
|
||||
typeof data !== "object"
|
||||
|| data === null
|
||||
|| data instanceof Array
|
||||
|| "locations" in data && !isChangeArray(data.locations)
|
||||
|| "events" in data && !isChangeArray(data.events)
|
||||
|| "roles" in data && !isChangeArray(data.roles)
|
||||
|| "rota" in data && !isChangeArray(data.rota)
|
||||
)
|
||||
throw new Error("Invalid patch data")
|
||||
return data // TODO: Actually validate the whole structure with e.g ajv or zod
|
||||
}
|
||||
import { broadcastEvent } from "~/server/streams";
|
||||
import { apiScheduleSchema } from "~/shared/types/api";
|
||||
import { applyUpdatesToArray } from "~/shared/utils/update";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireServerSession(event);
|
||||
|
@ -53,22 +20,43 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
const { success, error, data: update } = apiScheduleSchema.safeParse(await readBody(event));
|
||||
if (!success) {
|
||||
throw createError({
|
||||
status: 400,
|
||||
statusText: "Bad Request",
|
||||
message: z.prettifyError(error),
|
||||
});
|
||||
}
|
||||
|
||||
if (update.deleted) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Not implemented",
|
||||
});
|
||||
}
|
||||
|
||||
const schedule = await readSchedule();
|
||||
const patch = await readValidatedBody(event, isPatch);
|
||||
|
||||
if (schedule.deleted) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Not implemented",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate edit restrictions for crew
|
||||
if (account.type === "crew") {
|
||||
if (patch.locations?.length) {
|
||||
if (update.locations?.length) {
|
||||
throw createError({
|
||||
status: 403,
|
||||
statusMessage: "Forbidden",
|
||||
message: "Only admin accounts can edit locations.",
|
||||
});
|
||||
}
|
||||
for (const event of patch.events ?? []) {
|
||||
const id = event.op === "set" ? event.data.id : event.id;
|
||||
const original = schedule.events.find(e => e.id === id);
|
||||
if (original && !original.crew) {
|
||||
for (const event of update.events ?? []) {
|
||||
const original = schedule.events?.find(e => e.id === event.id);
|
||||
if (original && !original.deleted && !original.crew) {
|
||||
throw createError({
|
||||
status: 403,
|
||||
statusMessage: "Forbidden",
|
||||
|
@ -78,11 +66,30 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (patch.events) applyChangeArray(patch.events, schedule.events);
|
||||
if (patch.locations) applyChangeArray(patch.locations, schedule.locations);
|
||||
if (patch.roles) applyChangeArray(patch.roles, schedule.roles = schedule.roles ?? []);
|
||||
if (patch.rota) applyChangeArray(patch.rota, schedule.rota = schedule.rota ?? []);
|
||||
// Update schedule
|
||||
const updatedFrom = schedule.updatedAt;
|
||||
update.updatedAt = new Date().toISOString();
|
||||
if (update.events) {
|
||||
for (const event of update.events) event.updatedAt = update.updatedAt;
|
||||
applyUpdatesToArray(update.events, schedule.events = schedule.events ?? []);
|
||||
}
|
||||
if (update.locations) {
|
||||
for (const location of update.locations) location.updatedAt = update.updatedAt;
|
||||
applyUpdatesToArray(update.locations, schedule.locations = schedule.locations ?? []);
|
||||
}
|
||||
if (update.roles) {
|
||||
for (const role of update.roles) role.updatedAt = update.updatedAt;
|
||||
applyUpdatesToArray(update.roles, schedule.roles = schedule.roles ?? []);
|
||||
}
|
||||
if (update.shifts) {
|
||||
for (const shift of update.shifts) shift.updatedAt = update.updatedAt;
|
||||
applyUpdatesToArray(update.shifts, schedule.shifts = schedule.shifts ?? []);
|
||||
}
|
||||
|
||||
await writeSchedule(schedule);
|
||||
await broadcastUpdate(schedule);
|
||||
await broadcastEvent({
|
||||
type: "schedule-update",
|
||||
updatedFrom,
|
||||
data: update,
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue