307 lines
9 KiB
TypeScript
307 lines
9 KiB
TypeScript
import {
|
|
pgTable,
|
|
text,
|
|
uuid,
|
|
timestamp,
|
|
varchar,
|
|
unique,
|
|
check,
|
|
primaryKey,
|
|
index,
|
|
boolean,
|
|
integer,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
import { sql, relations } from "drizzle-orm";
|
|
|
|
import {
|
|
SUPPORTED_POS,
|
|
SUPPORTED_LANGUAGE_CODES,
|
|
CEFR_LEVELS,
|
|
SUPPORTED_DECK_TYPES,
|
|
DIFFICULTY_LEVELS,
|
|
LOBBY_STATUSES,
|
|
} from "@lila/shared";
|
|
|
|
export const terms = pgTable(
|
|
"terms",
|
|
{
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
source: varchar({ length: 50 }), // 'omw', 'wiktionary', null for manual
|
|
source_id: text(), // synset_id value for omw, wiktionary QID, etc.
|
|
pos: varchar({ length: 20 }).notNull(),
|
|
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(table) => [
|
|
check(
|
|
"pos_check",
|
|
sql`${table.pos} IN (${sql.raw(SUPPORTED_POS.map((p) => `'${p}'`).join(", "))})`,
|
|
),
|
|
unique("unique_source_id").on(table.source, table.source_id),
|
|
index("idx_terms_source_pos").on(table.source, table.pos),
|
|
],
|
|
);
|
|
|
|
export const term_glosses = pgTable(
|
|
"term_glosses",
|
|
{
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
term_id: uuid()
|
|
.notNull()
|
|
.references(() => terms.id, { onDelete: "cascade" }),
|
|
language_code: varchar({ length: 10 }).notNull(),
|
|
text: text().notNull(),
|
|
description: text(),
|
|
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(table) => [
|
|
unique("unique_term_gloss").on(table.term_id, table.language_code),
|
|
check(
|
|
"language_code_check",
|
|
sql`${table.language_code} IN (${sql.raw(SUPPORTED_LANGUAGE_CODES.map((l) => `'${l}'`).join(", "))})`,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const translations = pgTable(
|
|
"translations",
|
|
{
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
term_id: uuid()
|
|
.notNull()
|
|
.references(() => terms.id, { onDelete: "cascade" }),
|
|
language_code: varchar({ length: 10 }).notNull(),
|
|
text: text().notNull(),
|
|
cefr_level: varchar({ length: 2 }),
|
|
difficulty: varchar({ length: 20 }),
|
|
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(table) => [
|
|
unique("unique_translations").on(
|
|
table.term_id,
|
|
table.language_code,
|
|
table.text,
|
|
),
|
|
check(
|
|
"language_code_check",
|
|
sql`${table.language_code} IN (${sql.raw(SUPPORTED_LANGUAGE_CODES.map((l) => `'${l}'`).join(", "))})`,
|
|
),
|
|
check(
|
|
"cefr_check",
|
|
sql`${table.cefr_level} IN (${sql.raw(CEFR_LEVELS.map((l) => `'${l}'`).join(", "))})`,
|
|
),
|
|
check(
|
|
"difficulty_check",
|
|
sql`${table.difficulty} IN (${sql.raw(DIFFICULTY_LEVELS.map((d) => `'${d}'`).join(", "))})`,
|
|
),
|
|
index("idx_translations_lang").on(
|
|
table.language_code,
|
|
table.difficulty,
|
|
table.cefr_level,
|
|
table.term_id,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const decks = pgTable(
|
|
"decks",
|
|
{
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
name: text().notNull(),
|
|
description: text(),
|
|
source_language: varchar({ length: 10 }).notNull(),
|
|
validated_languages: varchar({ length: 10 }).array().notNull().default([]),
|
|
type: varchar({ length: 20 }).notNull(),
|
|
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
},
|
|
(table) => [
|
|
check(
|
|
"source_language_check",
|
|
sql`${table.source_language} IN (${sql.raw(SUPPORTED_LANGUAGE_CODES.map((l) => `'${l}'`).join(", "))})`,
|
|
),
|
|
check(
|
|
"validated_languages_check",
|
|
sql`validated_languages <@ ARRAY[${sql.raw(SUPPORTED_LANGUAGE_CODES.map((l) => `'${l}'`).join(", "))}]::varchar[]`,
|
|
),
|
|
check(
|
|
"validated_languages_excludes_source",
|
|
sql`NOT (${table.source_language} = ANY(${table.validated_languages}))`,
|
|
),
|
|
check(
|
|
"deck_type_check",
|
|
sql`${table.type} IN (${sql.raw(SUPPORTED_DECK_TYPES.map((t) => `'${t}'`).join(", "))})`,
|
|
),
|
|
unique("unique_deck_name").on(table.name, table.source_language),
|
|
index("idx_decks_type").on(table.type, table.source_language),
|
|
],
|
|
);
|
|
|
|
export const deck_terms = pgTable(
|
|
"deck_terms",
|
|
{
|
|
deck_id: uuid()
|
|
.notNull()
|
|
.references(() => decks.id, { onDelete: "cascade" }),
|
|
term_id: uuid()
|
|
.notNull()
|
|
.references(() => terms.id, { onDelete: "cascade" }),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.deck_id, table.term_id] })],
|
|
);
|
|
|
|
export const topics = pgTable("topics", {
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
slug: varchar({ length: 50 }).notNull().unique(),
|
|
label: text().notNull(),
|
|
description: text(),
|
|
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
});
|
|
|
|
export const term_topics = pgTable(
|
|
"term_topics",
|
|
{
|
|
term_id: uuid()
|
|
.notNull()
|
|
.references(() => terms.id, { onDelete: "cascade" }),
|
|
topic_id: uuid()
|
|
.notNull()
|
|
.references(() => topics.id, { onDelete: "cascade" }),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.term_id, table.topic_id] })],
|
|
);
|
|
|
|
export const user = pgTable("user", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
email: text("email").notNull().unique(),
|
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
image: text("image"),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at")
|
|
.defaultNow()
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
});
|
|
|
|
export const session = pgTable(
|
|
"session",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
expiresAt: timestamp("expires_at").notNull(),
|
|
token: text("token").notNull().unique(),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at")
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
ipAddress: text("ip_address"),
|
|
userAgent: text("user_agent"),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
},
|
|
(table) => [index("session_userId_idx").on(table.userId)],
|
|
);
|
|
|
|
export const account = pgTable(
|
|
"account",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
accountId: text("account_id").notNull(),
|
|
providerId: text("provider_id").notNull(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
accessToken: text("access_token"),
|
|
refreshToken: text("refresh_token"),
|
|
idToken: text("id_token"),
|
|
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
scope: text("scope"),
|
|
password: text("password"),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at")
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index("account_userId_idx").on(table.userId)],
|
|
);
|
|
|
|
export const verification = pgTable(
|
|
"verification",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
identifier: text("identifier").notNull(),
|
|
value: text("value").notNull(),
|
|
expiresAt: timestamp("expires_at").notNull(),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at")
|
|
.defaultNow()
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
|
);
|
|
|
|
export const userRelations = relations(user, ({ many }) => ({
|
|
sessions: many(session),
|
|
accounts: many(account),
|
|
}));
|
|
|
|
export const sessionRelations = relations(session, ({ one }) => ({
|
|
user: one(user, { fields: [session.userId], references: [user.id] }),
|
|
}));
|
|
|
|
export const accountRelations = relations(account, ({ one }) => ({
|
|
user: one(user, { fields: [account.userId], references: [user.id] }),
|
|
}));
|
|
|
|
export const lobbies = pgTable(
|
|
"lobbies",
|
|
{
|
|
id: uuid().primaryKey().defaultRandom(),
|
|
code: varchar({ length: 10 }).notNull().unique(),
|
|
hostUserId: text("host_user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
status: varchar({ length: 20 }).notNull().default("waiting"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
check(
|
|
"lobby_status_check",
|
|
sql`${table.status} IN (${sql.raw(LOBBY_STATUSES.map((s) => `'${s}'`).join(", "))})`,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const lobby_players = pgTable(
|
|
"lobby_players",
|
|
{
|
|
lobbyId: uuid("lobby_id")
|
|
.notNull()
|
|
.references(() => lobbies.id, { onDelete: "cascade" }),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
score: integer().notNull().default(0),
|
|
joinedAt: timestamp("joined_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.lobbyId, table.userId] })],
|
|
);
|
|
|
|
export const lobbyRelations = relations(lobbies, ({ one, many }) => ({
|
|
host: one(user, { fields: [lobbies.hostUserId], references: [user.id] }),
|
|
players: many(lobby_players),
|
|
}));
|
|
|
|
export const lobbyPlayersRelations = relations(lobby_players, ({ one }) => ({
|
|
lobby: one(lobbies, {
|
|
fields: [lobby_players.lobbyId],
|
|
references: [lobbies.id],
|
|
}),
|
|
user: one(user, { fields: [lobby_players.userId], references: [user.id] }),
|
|
}));
|