mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Handle and test slug reconciliation & conflicts
This commit is contained in:
parent
caa394e0da
commit
c8c6cccf6a
@ -1,6 +1,8 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import { Movie, SeedMovie } from "~/models/movie";
|
||||
import { seedMovie, SeedMovieResponse } from "./movies";
|
||||
import { Resource } from "~/models/utils";
|
||||
import { comment } from "~/utils";
|
||||
|
||||
export const seed = new Elysia()
|
||||
.model({
|
||||
@ -13,7 +15,7 @@ export const seed = new Elysia()
|
||||
"/movies",
|
||||
async ({ body, error }) => {
|
||||
const { status, ...ret } = await seedMovie(body);
|
||||
return error(status === "created" ? 201 : 200, ret);
|
||||
return error(status, ret);
|
||||
},
|
||||
{
|
||||
body: "seed-movie",
|
||||
@ -24,6 +26,13 @@ export const seed = new Elysia()
|
||||
},
|
||||
201: { ...SeedMovieResponse, description: "Created a new movie." },
|
||||
400: "error",
|
||||
409: {
|
||||
...Resource,
|
||||
description: comment`
|
||||
A movie with the same slug but a different air date already exists.
|
||||
Change the slug and re-run the request.
|
||||
`,
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
tags: ["movies"],
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { inArray, sql } from "drizzle-orm";
|
||||
import { inArray, sql, eq } from "drizzle-orm";
|
||||
import { t } from "elysia";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
@ -29,7 +29,9 @@ export type SeedMovieResponse = typeof SeedMovieResponse.static;
|
||||
|
||||
export const seedMovie = async (
|
||||
seed: SeedMovie,
|
||||
): Promise<SeedMovieResponse & { status: "created" | "updated" }> => {
|
||||
): Promise<
|
||||
SeedMovieResponse & { status: "Created" | "OK" | "Conflict" }
|
||||
> => {
|
||||
const { translations, videos: vids, ...bMovie } = seed;
|
||||
|
||||
const ret = await db.transaction(async (tx) => {
|
||||
@ -39,29 +41,57 @@ export const seedMovie = async (
|
||||
nextRefresh: guessNextRefresh(bMovie.airDate ?? new Date()),
|
||||
...bMovie,
|
||||
};
|
||||
const [ret] = await tx
|
||||
.insert(shows)
|
||||
.values(movie)
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: conflictUpdateAllExcept(shows, ["pk", "id", "slug", "createdAt"]),
|
||||
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
|
||||
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
|
||||
})
|
||||
.returning({
|
||||
pk: shows.pk,
|
||||
id: shows.id,
|
||||
slug: shows.slug,
|
||||
startAir: shows.startAir,
|
||||
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
|
||||
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
|
||||
});
|
||||
if (ret.updated) {
|
||||
// TODO: if updated, differenciates updates with conflicts.
|
||||
// if the start year is different or external ids, it's a conflict.
|
||||
// if (getYear(ret.startAir) === getYear(movie.startAir)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const insert = () =>
|
||||
tx
|
||||
.insert(shows)
|
||||
.values(movie)
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: conflictUpdateAllExcept(shows, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
|
||||
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
|
||||
})
|
||||
.returning({
|
||||
pk: shows.pk,
|
||||
id: shows.id,
|
||||
slug: shows.slug,
|
||||
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
|
||||
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
|
||||
});
|
||||
let [ret] = await insert();
|
||||
if (!ret) {
|
||||
// ret is undefined when the conflict's where return false (meaning we have
|
||||
// a conflicting slug but a different air year.
|
||||
// try to insert adding the year at the end of the slug.
|
||||
if (
|
||||
movie.startAir &&
|
||||
!movie.slug.endsWith(`${getYear(movie.startAir)}`)
|
||||
) {
|
||||
movie.slug = `${movie.slug}-${getYear(movie.startAir)}`;
|
||||
[ret] = await insert();
|
||||
}
|
||||
|
||||
// if at this point ret is still undefined, we could not reconciliate.
|
||||
// simply bail and let the caller handle this.
|
||||
if (!ret) {
|
||||
const [{ id }] = await db
|
||||
.select({ id: shows.id })
|
||||
.from(shows)
|
||||
.where(eq(shows.slug, movie.slug))
|
||||
.limit(1);
|
||||
return {
|
||||
status: "Conflict" as const,
|
||||
id,
|
||||
slug: movie.slug,
|
||||
videos: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// even if never shown to the user, a movie still has an entry.
|
||||
@ -112,6 +142,8 @@ export const seedMovie = async (
|
||||
return { ...ret, entry: entry.pk };
|
||||
});
|
||||
|
||||
if (ret.status === "Conflict") return ret;
|
||||
|
||||
let retVideos: { slug: string }[] = [];
|
||||
if (vids) {
|
||||
retVideos = await db
|
||||
@ -139,14 +171,13 @@ export const seedMovie = async (
|
||||
}
|
||||
|
||||
return {
|
||||
status: ret.updated ? "updated" : "created",
|
||||
status: ret.updated ? "Ok" : "Created",
|
||||
id: ret.id,
|
||||
slug: ret.slug,
|
||||
videos: retVideos,
|
||||
};
|
||||
};
|
||||
|
||||
function getYear(date?: string | null) {
|
||||
if (!date) return null;
|
||||
function getYear(date: string) {
|
||||
return new Date(date).getUTCFullYear();
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ describe("Movie seeding", () => {
|
||||
|
||||
const [resp, body] = await createMovie({
|
||||
...dune,
|
||||
airDate: "2159-12-09",
|
||||
runtime: 200_000,
|
||||
translations: {
|
||||
...dune.translations,
|
||||
en: { ...dune.translations.en, description: "edited translation" },
|
||||
@ -85,23 +85,49 @@ describe("Movie seeding", () => {
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.id).toBeString();
|
||||
expect(body.slug).toBe("dune");
|
||||
expect(body.videos).toBe([]);
|
||||
expect(edited.startAir).toBe("2159-12-09");
|
||||
expect(body.videos).toBeArrayOfSize(0);
|
||||
expect(edited.runtime).toBe(200_000);
|
||||
expect(edited.status).toBe(dune.status);
|
||||
expect(translations).toMatchObject({
|
||||
language: "en",
|
||||
expect(translations.find((x) => x.language === "en")).toMatchObject({
|
||||
name: dune.translations.en.name,
|
||||
description: "edited translation",
|
||||
});
|
||||
expect(translations).toMatchObject({
|
||||
language: "fr",
|
||||
expect(translations.find((x) => x.language === "fr")).toMatchObject({
|
||||
name: "dune-but-in-french",
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
test.todo("Conflicting slug auto-correct", async () => {});
|
||||
test.todo("Conflict in slug+year fails", async () => {});
|
||||
it("Conflicting slug auto-correct", async () => {
|
||||
// confirm that db is in the correct state (from previous tests)
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(shows)
|
||||
.where(eq(shows.slug, dune.slug))
|
||||
.limit(1);
|
||||
expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate });
|
||||
|
||||
const [resp, body] = await createMovie({ ...dune, airDate: "2158-12-13" });
|
||||
expectStatus(resp, body).toBe(200);
|
||||
expect(body.id).toBeString();
|
||||
expect(body.slug).toBe("dune-2158");
|
||||
});
|
||||
|
||||
it("Conflict in slug w/out year fails", async () => {
|
||||
// confirm that db is in the correct state (from conflict auto-correct test)
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(shows)
|
||||
.where(eq(shows.slug, dune.slug))
|
||||
.limit(1);
|
||||
expect(existing).toMatchObject({ slug: dune.slug, startAir: dune.airDate });
|
||||
|
||||
const [resp, body] = await createMovie({ ...dune, airDate: null });
|
||||
expectStatus(resp, body).toBe(409);
|
||||
expect(body.id).toBe(existing.id);
|
||||
expect(body.slug).toBe(existing.slug);
|
||||
});
|
||||
|
||||
test.todo("Missing videos send info", async () => {});
|
||||
test.todo("Schema error", async () => {});
|
||||
test.todo("Invalid translation name", async () => {});
|
||||
|
Loading…
x
Reference in New Issue
Block a user