Validate language tags

This commit is contained in:
Zoe Roux 2024-12-08 22:24:05 +01:00
parent cdceb1a734
commit 0c0628529c
No known key found for this signature in database
5 changed files with 102 additions and 3 deletions

View File

@ -1,7 +1,7 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { Movie, SeedMovie } from "~/models/movie"; import { Movie, SeedMovie } from "~/models/movie";
import { seedMovie, SeedMovieResponse } from "./movies"; import { seedMovie, SeedMovieResponse } from "./movies";
import { Resource } from "~/models/utils"; import { Resource, validateTranslations } from "~/models/utils";
import { comment } from "~/utils"; import { comment } from "~/utils";
import { KError } from "~/models/error"; import { KError } from "~/models/error";
@ -14,6 +14,9 @@ export const seed = new Elysia()
.post( .post(
"/movies", "/movies",
async ({ body, error }) => { async ({ body, error }) => {
const err = validateTranslations(body.translations);
if (err) return error(400, err);
const { status, ...ret } = await seedMovie(body); const { status, ...ret } = await seedMovie(body);
return error(status, ret); return error(status, ret);
}, },

View File

@ -1,4 +1,4 @@
import { sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
check, check,
date, date,
@ -108,3 +108,14 @@ export const showTranslations = schema.table(
}, },
(t) => [primaryKey({ columns: [t.pk, t.language] })], (t) => [primaryKey({ columns: [t.pk, t.language] })],
); );
export const showsRelations = relations(shows, ({ many }) => ({
translations: many(showTranslations, { relationName: "showTranslations" }),
}));
export const showsTrRelations = relations(showTranslations, ({ one }) => ({
show: one(shows, {
relationName: "showTranslations",
fields: [showTranslations.pk],
references: [shows.pk],
}),
}));

View File

@ -12,6 +12,7 @@ import { videos } from "./controllers/videos";
import { db } from "./db"; import { db } from "./db";
import { Image } from "./models/utils"; import { Image } from "./models/utils";
import { comment } from "./utils"; import { comment } from "./utils";
import { base } from "./base";
await migrate(db, { migrationsSchema: "kyoo", migrationsFolder: "./drizzle" }); await migrate(db, { migrationsSchema: "kyoo", migrationsFolder: "./drizzle" });
@ -37,6 +38,7 @@ if (!secret) {
} }
const app = new Elysia() const app = new Elysia()
.use(base)
.use(jwt({ secret })) .use(jwt({ secret }))
.use( .use(
swagger({ swagger({

View File

@ -1,6 +1,28 @@
import { FormatRegistry } from "@sinclair/typebox"; import { FormatRegistry } from "@sinclair/typebox";
import { t } from "elysia"; import { t } from "elysia";
import { comment } from "../../utils"; import { comment } from "../../utils";
import type { KError } from "../error";
export const validateTranslations = <T extends object>(
translations: Record<string, T>,
): KError | null => {
for (const lang of Object.keys(translations)) {
try {
const valid = new Intl.Locale(lang).baseName;
if (lang !== valid) {
translations[valid] = translations[lang];
delete translations[lang];
}
} catch (e) {
return {
status: 400,
message: `Invalid translation name: '${lang}'.`,
details: null,
};
}
}
return null;
};
FormatRegistry.Set("language", (lang) => { FormatRegistry.Set("language", (lang) => {
try { try {

View File

@ -1,13 +1,15 @@
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import Elysia from "elysia"; import Elysia from "elysia";
import { base } from "~/base";
import { seed } from "~/controllers/seed"; import { seed } from "~/controllers/seed";
import { db } from "~/db"; import { db } from "~/db";
import { shows, showTranslations, videos } from "~/db/schema"; import { shows, showTranslations, videos } from "~/db/schema";
import { bubble } from "~/models/examples";
import { dune, duneVideo } from "~/models/examples/dune-2021"; import { dune, duneVideo } from "~/models/examples/dune-2021";
import type { SeedMovie } from "~/models/movie"; import type { SeedMovie } from "~/models/movie";
const app = new Elysia().use(seed); const app = new Elysia().use(base).use(seed);
const createMovie = async (movie: SeedMovie) => { const createMovie = async (movie: SeedMovie) => {
const resp = await app.handle( const resp = await app.handle(
new Request("http://localhost/movies", { new Request("http://localhost/movies", {
@ -157,6 +159,65 @@ describe("Movie seeding", () => {
expect(body.details).toBeObject(); expect(body.details).toBeObject();
// TODO: handle additional fields too // TODO: handle additional fields too
}); });
it("Invalid translation name", async () => {
const [resp, body] = await createMovie({
...dune,
translations: {
...dune.translations,
test: {
name: "foo",
description: "bar",
tags: [],
aliases: [],
tagline: "toto",
banner: null,
poster: null,
thumbnail: null,
logo: null,
trailerUrl: null,
},
},
});
expectStatus(resp, body).toBe(400);
expect(body.status).toBe(400);
expect(body.message).toBe("Invalid translation name: 'test'.");
});
it("Correct translations casing.", async () => {
const [resp, body] = await createMovie({
...bubble,
slug: "casing-test",
translations: {
"en-us": {
name: "foo",
description: "bar",
tags: [],
aliases: [],
tagline: "toto",
banner: null,
poster: null,
thumbnail: null,
logo: null,
trailerUrl: null,
},
},
});
expect(resp.status).toBeWithin(200, 299);
expect(body.id).toBeString();
const ret = await db.query.shows.findFirst({
where: eq(shows.id, body.id),
with: { translations: true },
});
expect(ret!.translations).toBeArrayOfSize(1);
expect(ret!.translations[0]).toMatchObject({
language: "en-US",
name: "foo",
});
});
test.todo("Create correct video slug (version)", async () => {}); test.todo("Create correct video slug (version)", async () => {});
test.todo("Create correct video slug (part)", async () => {}); test.todo("Create correct video slug (part)", async () => {});
test.todo("Create correct video slug (rendering)", async () => {}); test.todo("Create correct video slug (rendering)", async () => {});