Make movie get work

This commit is contained in:
Zoe Roux 2024-12-15 18:28:05 +01:00
parent 9e1afca9ec
commit eea0f688a0
No known key found for this signature in database
4 changed files with 126 additions and 53 deletions

View File

@ -11,6 +11,7 @@ export const base = new Elysia({ name: "base" })
} as KError; } as KError;
} }
if (code === "INTERNAL_SERVER_ERROR") { if (code === "INTERNAL_SERVER_ERROR") {
console.error(error);
return { return {
status: 500, status: 500,
message: error.message, message: error.message,

View File

@ -1,57 +1,45 @@
import { and, eq, sql } from "drizzle-orm";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { Movie, MovieTranslation } from "../models/movie"; import { KError } from "~/models/error";
import { isUuid, processLanguages } from "~/models/utils";
import { comment } from "~/utils";
import { db } from "../db"; import { db } from "../db";
import { shows, showTranslations } from "../db/schema/shows"; import { shows, showTranslations } from "../db/schema/shows";
import { eq, and, sql, or } from "drizzle-orm";
import { getColumns } from "../db/schema/utils"; import { getColumns } from "../db/schema/utils";
import { bubble } from "../models/examples"; import { bubble } from "../models/examples";
import { comment } from "~/utils"; import { Movie, MovieTranslation } from "../models/movie";
import { processLanguages } from "~/models/utils";
const translations = db // drizzle is bugged and doesn't allow js arrays to be used in raw sql.
.selectDistinctOn([showTranslations.pk]) export function sqlarr(array: unknown[]) {
.from(showTranslations) return `{${array.map((item) => `"${item}"`).join(",")}}`;
// .where( }
// or(
// eq(showTranslations.language, sql`any(${sql.placeholder("langs")})`), const getTranslationQuery = (languages: string[]) => {
// eq(showTranslations.language, shows.originalLanguage), const fallback = languages.includes("*");
// ), const query = db
// ) .selectDistinctOn([showTranslations.pk])
.orderBy( .from(showTranslations)
showTranslations.pk, .where(
sql`array_position(${sql.placeholder("langs")}, ${showTranslations.language})`, fallback
) ? undefined
.as("t"); : eq(showTranslations.language, sql`any(${sqlarr(languages)})`),
)
.orderBy(
showTranslations.pk,
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
)
.as("t");
const { pk, ...col } = getColumns(query);
return [query, col] as const;
};
const { pk: _, kind, startAir, endAir, ...moviesCol } = getColumns(shows); const { pk: _, kind, startAir, endAir, ...moviesCol } = getColumns(shows);
const { pk, language, ...translationsCol } = getColumns(translations);
const findMovie = db
.select({
...moviesCol,
...translationsCol,
airDate: startAir,
})
.from(shows)
.innerJoin(translations, eq(shows.pk, translations.pk))
.where(
and(
eq(shows.kind, "movie"),
// or(
// eq(shows.id, sql.placeholder("id")),
eq(shows.slug, sql.placeholder("id")),
// ),
),
)
// .orderBy()
.limit(1)
.prepare("findMovie");
export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
.model({ .model({
movie: Movie, movie: Movie,
"movie-translation": MovieTranslation, "movie-translation": MovieTranslation,
error: t.Object({}),
}) })
.guard({ .guard({
params: t.Object({ params: t.Object({
@ -61,7 +49,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
}), }),
}), }),
headers: t.Object({ headers: t.Object({
"Accept-Language": t.String({ "accept-language": t.String({
default: "*", default: "*",
examples: "en-us, ja;q=0.5", examples: "en-us, ja;q=0.5",
description: comment` description: comment`
@ -71,26 +59,66 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
`, `,
}), }),
}), }),
response: { 200: "movie", 404: "error" }, response: {
200: "movie",
404: {
...KError,
description: "No movie found with the given id or slug.",
},
422: {
...KError,
description: comment`
The Accept-Language header can't be satisfied (all languages listed are
unavailable). Try with another languages or add * to the list of languages
to fallback to any language.
`,
},
},
}) })
.get( .get(
"/:id", "/:id",
async ({ async ({
params: { id }, params: { id },
headers: { "Accept-Language": languages }, headers: { "accept-language": languages },
error, error,
set,
}) => { }) => {
const langs = processLanguages(languages); const langs = processLanguages(languages);
console.log(langs); const [transQ, transCol] = getTranslationQuery(langs);
console.log(findMovie.getQuery());
const ret = await findMovie.execute({ id, langs }); const idFilter = isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id);
console.log(ret);
if (ret.length !== 1) return error(404, {}); const [ret] = await db
return ret[0]; .select({
...moviesCol,
...transCol,
airDate: startAir,
})
.from(shows)
.leftJoin(transQ, eq(shows.pk, transQ.pk))
.where(and(eq(shows.kind, "movie"), idFilter))
.limit(1);
if (!ret) {
return error(404, {
status: 404,
message: "Movie not found",
details: undefined,
});
}
if (!ret.language) {
return error(422, {
status: 422,
message: "Accept-Language header could not be satisfied.",
details: undefined,
});
}
set.headers["content-language"] = ret.language;
return ret;
}, },
{ {
detail: { detail: {
description: "Get a movie by id or slug", description: "Get a movie by id or slug",
}, },
}, },
) );

View File

@ -1,4 +1,5 @@
import { FormatRegistry } from "@sinclair/typebox"; import { FormatRegistry } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import { t } from "elysia"; import { t } from "elysia";
export const slugPattern = "^[a-z0-9-]+$"; export const slugPattern = "^[a-z0-9-]+$";
@ -11,3 +12,6 @@ export const Resource = t.Object({
id: t.String({ format: "uuid" }), id: t.String({ format: "uuid" }),
slug: t.String({ format: "slug" }), slug: t.String({ format: "slug" }),
}); });
const checker = TypeCompiler.Compile(t.String({ format: "uuid" }));
export const isUuid = (id: string) => checker.Check(id);

View File

@ -22,6 +22,8 @@ const getMovie = async (id: string, langs: string) => {
return [resp, body] as const; return [resp, body] as const;
}; };
let bubbleId = "";
function expectStatus(resp: Response, body: object) { function expectStatus(resp: Response, body: object) {
const matcher = expect({ ...body, status: resp.status }); const matcher = expect({ ...body, status: resp.status });
return { return {
@ -41,12 +43,50 @@ describe("Get movie", () => {
name: bubble.translations.en.name, name: bubble.translations.en.name,
}); });
}); });
it("Retrive by id", async () => {
const [resp, body] = await getMovie(bubbleId, "en");
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
id: bubbleId,
slug: bubble.slug,
name: bubble.translations.en.name,
});
});
it("Get non available translation", async () => {
const [resp, body] = await getMovie(bubble.slug, "fr");
expectStatus(resp, body).toBe(422);
expect(body).toMatchObject({
status: 422,
});
});
it("Get first available language", async () => {
const [resp, body] = await getMovie(bubble.slug, "fr,en");
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
slug: bubble.slug,
name: bubble.translations.en.name,
});
expect(resp.headers.get("Content-Language")).toBe("en");
});
it("Use language fallback", async () => {
const [resp, body] = await getMovie(bubble.slug, "fr,ja,*");
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
slug: bubble.slug,
name: bubble.translations.en.name,
});
expect(resp.headers.get("Content-Language")).toBe("en");
});
}); });
beforeAll(async () => { beforeAll(async () => {
const ret = await seedMovie(bubble); const ret = await seedMovie(bubble);
console.log("seed bubble", ret); bubbleId = ret.id;
}); });
afterAll(async () => { afterAll(async () => {
// await db.delete(shows).where(eq(shows.slug, bubble.slug)); await db.delete(shows).where(eq(shows.slug, bubble.slug));
}); });