feat: add production deployment config
- Add docker-compose.prod.yml and Caddyfile for Caddy reverse proxy - Add production stages to frontend Dockerfile (nginx for static files) - Fix monorepo package exports for production builds (dist/src paths) - Add CORS_ORIGIN env var for cross-origin config - Add Better Auth baseURL, cookie domain, and trusted origins from env - Use VITE_API_URL for API calls in auth-client and play route - Add credentials: include for cross-origin fetch requests - Remove unused users table from schema
This commit is contained in:
parent
3f7bc4111e
commit
bc38137a12
20 changed files with 421515 additions and 34 deletions
|
|
@ -1,5 +1,12 @@
|
||||||
DATABASE_URL=postgres://postgres:mypassword@db-host:5432/postgres
|
DATABASE_URL=postgres://postgres:mypassword@db-host:5432/databasename
|
||||||
|
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_DB=databasename
|
POSTGRES_DB=databasename
|
||||||
|
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
BETTER_AUTH_URL=
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
|
||||||
11
Caddyfile
Normal file
11
Caddyfile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
lilastudy.com {
|
||||||
|
reverse_proxy web:80
|
||||||
|
}
|
||||||
|
|
||||||
|
api.lilastudy.com {
|
||||||
|
reverse_proxy api:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
git.lilastudy.com {
|
||||||
|
reverse_proxy forgejo:3000
|
||||||
|
}
|
||||||
|
|
@ -32,11 +32,13 @@ RUN pnpm --filter api build
|
||||||
# 5. run
|
# 5. run
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
COPY --from=deps /app/packages/shared/package.json /app/packages/shared/package.json
|
COPY apps/api/package.json ./apps/api/
|
||||||
COPY --from=deps /app/packages/db/package.json /app/packages/db/package.json
|
COPY packages/shared/package.json ./packages/shared/
|
||||||
COPY --from=builder /app/apps/api/dist ./dist
|
COPY packages/db/package.json ./packages/db/
|
||||||
COPY --from=builder /app/packages/shared/dist /app/packages/shared/dist
|
COPY --from=builder /app/apps/api/dist ./apps/api/dist
|
||||||
COPY --from=builder /app/packages/db/dist /app/packages/db/dist
|
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
|
||||||
|
COPY --from=builder /app/packages/db/dist ./packages/db/dist
|
||||||
|
RUN pnpm install --frozen-lockfile --prod
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["node", "dist/server.js"]
|
CMD ["node", "apps/api/dist/src/server.js"]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/server.ts",
|
"dev": "tsx watch src/server.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/src/server.js",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ import cors from "cors";
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
|
|
||||||
app.use(cors({ origin: "http://localhost:5173", credentials: true }));
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: process.env["CORS_ORIGIN"] || "http://localhost:5173",
|
||||||
|
credentials: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
app.all("/api/auth/*splat", toNodeHandler(auth));
|
app.all("/api/auth/*splat", toNodeHandler(auth));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use("/api/v1", apiRouter);
|
app.use("/api/v1", apiRouter);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,19 @@ import { db } from "@lila/db";
|
||||||
import * as schema from "@lila/db/schema";
|
import * as schema from "@lila/db/schema";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
|
baseURL: process.env["BETTER_AUTH_URL"] || "http://localhost:3000",
|
||||||
|
advanced: {
|
||||||
|
cookiePrefix: "lila",
|
||||||
|
defaultCookieAttributes: {
|
||||||
|
...(process.env["COOKIE_DOMAIN"] && {
|
||||||
|
domain: process.env["COOKIE_DOMAIN"],
|
||||||
|
}),
|
||||||
|
secure: !!process.env["COOKIE_DOMAIN"],
|
||||||
|
sameSite: "lax" as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
database: drizzleAdapter(db, { provider: "pg", schema }),
|
database: drizzleAdapter(db, { provider: "pg", schema }),
|
||||||
trustedOrigins: ["http://localhost:5173"],
|
trustedOrigins: [process.env["CORS_ORIGIN"] || "http://localhost:5173"],
|
||||||
socialProviders: {
|
socialProviders: {
|
||||||
google: {
|
google: {
|
||||||
clientId: process.env["GOOGLE_CLIENT_ID"] as string,
|
clientId: process.env["GOOGLE_CLIENT_ID"] as string,
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,20 @@ COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . ./
|
COPY . ./
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
CMD ["pnpm", "--filter", "web", "dev", "--host"]
|
CMD ["pnpm", "--filter", "web", "dev", "--host"]
|
||||||
|
|
||||||
|
# 4. Build
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm install
|
||||||
|
ARG VITE_API_URL
|
||||||
|
ENV VITE_API_URL=$VITE_API_URL
|
||||||
|
RUN pnpm --filter shared build
|
||||||
|
RUN pnpm --filter web build
|
||||||
|
|
||||||
|
# 5. Production — just nginx serving static files
|
||||||
|
FROM nginx:alpine AS production
|
||||||
|
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
|
||||||
|
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
|
|
||||||
9
apps/web/nginx.conf
Normal file
9
apps/web/nginx.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: "http://localhost:3000",
|
baseURL: import.meta.env["VITE_API_URL"] || "http://localhost:3000",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { signIn, signOut, useSession } = authClient;
|
export const { signIn, signOut, useSession } = authClient;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const LoginPage = () => {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
signIn.social({
|
signIn.social({
|
||||||
provider: "github",
|
provider: "github",
|
||||||
callbackURL: "http://localhost:5173",
|
callbackURL: window.location.origin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -31,7 +31,7 @@ const LoginPage = () => {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
signIn.social({
|
signIn.social({
|
||||||
provider: "google",
|
provider: "google",
|
||||||
callbackURL: "http://localhost:5173",
|
callbackURL: window.location.origin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { GameSetup } from "../components/game/GameSetup";
|
||||||
import { authClient } from "../lib/auth-client";
|
import { authClient } from "../lib/auth-client";
|
||||||
|
|
||||||
function Play() {
|
function Play() {
|
||||||
|
const API_URL = import.meta.env["VITE_API_URL"] || "";
|
||||||
|
|
||||||
const [gameSession, setGameSession] = useState<GameSession | null>(null);
|
const [gameSession, setGameSession] = useState<GameSession | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||||
|
|
@ -15,9 +17,10 @@ function Play() {
|
||||||
|
|
||||||
const startGame = useCallback(async (settings: GameRequest) => {
|
const startGame = useCallback(async (settings: GameRequest) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await fetch("/api/v1/game/start", {
|
const response = await fetch(`${API_URL}/api/v1/game/start`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -42,9 +45,10 @@ function Play() {
|
||||||
const question = gameSession.questions[currentQuestionIndex];
|
const question = gameSession.questions[currentQuestionIndex];
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
const response = await fetch("/api/v1/game/answer", {
|
const response = await fetch(`${API_URL}/api/v1/game/answer`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sessionId: gameSession.sessionId,
|
sessionId: gameSession.sessionId,
|
||||||
questionId: question.questionId,
|
questionId: question.questionId,
|
||||||
|
|
|
||||||
91
docker-compose.prod.yml
Normal file
91
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
container_name: lila-caddy
|
||||||
|
image: caddy:2-alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- lila-network
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: lila-api
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./apps/api/Dockerfile
|
||||||
|
target: runner
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- lila-network
|
||||||
|
|
||||||
|
web:
|
||||||
|
container_name: lila-web
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./apps/web/Dockerfile
|
||||||
|
target: production
|
||||||
|
args:
|
||||||
|
VITE_API_URL: https://api.lilastudy.com
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lila-network
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: lila-database
|
||||||
|
image: postgres:18.3-alpine3.23
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- PGDATA=/var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- lila-db:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- lila-network
|
||||||
|
|
||||||
|
forgejo:
|
||||||
|
container_name: lila-forgejo
|
||||||
|
image: codeberg.org/forgejo/forgejo:11
|
||||||
|
volumes:
|
||||||
|
- forgejo-data:/data
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- lila-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lila-network:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
lila-db:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
|
forgejo-data:
|
||||||
|
|
@ -7,6 +7,24 @@
|
||||||
|
|
||||||
## problems+thoughts
|
## problems+thoughts
|
||||||
|
|
||||||
|
### docker credential helper
|
||||||
|
|
||||||
|
WARNING! Your credentials are stored unencrypted in '/home/languagedev/.docker/config.json'.
|
||||||
|
Configure a credential helper to remove this warning. See
|
||||||
|
https://docs.docker.com/go/credential-store/
|
||||||
|
|
||||||
|
### vps setup
|
||||||
|
|
||||||
|
monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
|
||||||
|
|
||||||
|
### cd/ci pipeline
|
||||||
|
|
||||||
|
forgejo actions? smth else? where docker registry, also forgejo?
|
||||||
|
|
||||||
|
### postgres backups
|
||||||
|
|
||||||
|
how?
|
||||||
|
|
||||||
### try now option
|
### try now option
|
||||||
|
|
||||||
there should be an option to try the app without an account so users can see what they would get when creating an account
|
there should be an option to try the app without an account so users can see what they would get when creating an account
|
||||||
|
|
|
||||||
1
packages/db/drizzle/0005_broad_mariko_yashida.sql
Normal file
1
packages/db/drizzle/0005_broad_mariko_yashida.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE "users" CASCADE;
|
||||||
935
packages/db/drizzle/meta/0005_snapshot.json
Normal file
935
packages/db/drizzle/meta/0005_snapshot.json
Normal file
|
|
@ -0,0 +1,935 @@
|
||||||
|
{
|
||||||
|
"id": "8f34bafa-cffc-4933-952f-64b46afa9c5c",
|
||||||
|
"prevId": "6455ad81-98c0-4f32-a2fa-0f99ce9ce8e5",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"account_userId_idx": {
|
||||||
|
"name": "account_userId_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.deck_terms": {
|
||||||
|
"name": "deck_terms",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"deck_id": {
|
||||||
|
"name": "deck_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"term_id": {
|
||||||
|
"name": "term_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"deck_terms_deck_id_decks_id_fk": {
|
||||||
|
"name": "deck_terms_deck_id_decks_id_fk",
|
||||||
|
"tableFrom": "deck_terms",
|
||||||
|
"tableTo": "decks",
|
||||||
|
"columnsFrom": [
|
||||||
|
"deck_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deck_terms_term_id_terms_id_fk": {
|
||||||
|
"name": "deck_terms_term_id_terms_id_fk",
|
||||||
|
"tableFrom": "deck_terms",
|
||||||
|
"tableTo": "terms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"term_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"deck_terms_deck_id_term_id_pk": {
|
||||||
|
"name": "deck_terms_deck_id_term_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"deck_id",
|
||||||
|
"term_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.decks": {
|
||||||
|
"name": "decks",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"source_language": {
|
||||||
|
"name": "source_language",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"validated_languages": {
|
||||||
|
"name": "validated_languages",
|
||||||
|
"type": "varchar(10)[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_decks_type": {
|
||||||
|
"name": "idx_decks_type",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "source_language",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"unique_deck_name": {
|
||||||
|
"name": "unique_deck_name",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"name",
|
||||||
|
"source_language"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"source_language_check": {
|
||||||
|
"name": "source_language_check",
|
||||||
|
"value": "\"decks\".\"source_language\" IN ('en', 'it')"
|
||||||
|
},
|
||||||
|
"validated_languages_check": {
|
||||||
|
"name": "validated_languages_check",
|
||||||
|
"value": "validated_languages <@ ARRAY['en', 'it']::varchar[]"
|
||||||
|
},
|
||||||
|
"validated_languages_excludes_source": {
|
||||||
|
"name": "validated_languages_excludes_source",
|
||||||
|
"value": "NOT (\"decks\".\"source_language\" = ANY(\"decks\".\"validated_languages\"))"
|
||||||
|
},
|
||||||
|
"deck_type_check": {
|
||||||
|
"name": "deck_type_check",
|
||||||
|
"value": "\"decks\".\"type\" IN ('grammar', 'media')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"session_userId_idx": {
|
||||||
|
"name": "session_userId_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.term_glosses": {
|
||||||
|
"name": "term_glosses",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"term_id": {
|
||||||
|
"name": "term_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language_code": {
|
||||||
|
"name": "language_code",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"term_glosses_term_id_terms_id_fk": {
|
||||||
|
"name": "term_glosses_term_id_terms_id_fk",
|
||||||
|
"tableFrom": "term_glosses",
|
||||||
|
"tableTo": "terms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"term_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"unique_term_gloss": {
|
||||||
|
"name": "unique_term_gloss",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"term_id",
|
||||||
|
"language_code"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"language_code_check": {
|
||||||
|
"name": "language_code_check",
|
||||||
|
"value": "\"term_glosses\".\"language_code\" IN ('en', 'it')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.term_topics": {
|
||||||
|
"name": "term_topics",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"term_id": {
|
||||||
|
"name": "term_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"topic_id": {
|
||||||
|
"name": "topic_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"term_topics_term_id_terms_id_fk": {
|
||||||
|
"name": "term_topics_term_id_terms_id_fk",
|
||||||
|
"tableFrom": "term_topics",
|
||||||
|
"tableTo": "terms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"term_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"term_topics_topic_id_topics_id_fk": {
|
||||||
|
"name": "term_topics_topic_id_topics_id_fk",
|
||||||
|
"tableFrom": "term_topics",
|
||||||
|
"tableTo": "topics",
|
||||||
|
"columnsFrom": [
|
||||||
|
"topic_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"term_topics_term_id_topic_id_pk": {
|
||||||
|
"name": "term_topics_term_id_topic_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"term_id",
|
||||||
|
"topic_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.terms": {
|
||||||
|
"name": "terms",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"source_id": {
|
||||||
|
"name": "source_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"pos": {
|
||||||
|
"name": "pos",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_terms_source_pos": {
|
||||||
|
"name": "idx_terms_source_pos",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "source",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "pos",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"unique_source_id": {
|
||||||
|
"name": "unique_source_id",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"source",
|
||||||
|
"source_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"pos_check": {
|
||||||
|
"name": "pos_check",
|
||||||
|
"value": "\"terms\".\"pos\" IN ('noun', 'verb')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.topics": {
|
||||||
|
"name": "topics",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"name": "label",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"topics_slug_unique": {
|
||||||
|
"name": "topics_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.translations": {
|
||||||
|
"name": "translations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"term_id": {
|
||||||
|
"name": "term_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language_code": {
|
||||||
|
"name": "language_code",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cefr_level": {
|
||||||
|
"name": "cefr_level",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"difficulty": {
|
||||||
|
"name": "difficulty",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_translations_lang": {
|
||||||
|
"name": "idx_translations_lang",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "language_code",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "difficulty",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "cefr_level",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "term_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"translations_term_id_terms_id_fk": {
|
||||||
|
"name": "translations_term_id_terms_id_fk",
|
||||||
|
"tableFrom": "translations",
|
||||||
|
"tableTo": "terms",
|
||||||
|
"columnsFrom": [
|
||||||
|
"term_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"unique_translations": {
|
||||||
|
"name": "unique_translations",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"term_id",
|
||||||
|
"language_code",
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"language_code_check": {
|
||||||
|
"name": "language_code_check",
|
||||||
|
"value": "\"translations\".\"language_code\" IN ('en', 'it')"
|
||||||
|
},
|
||||||
|
"cefr_check": {
|
||||||
|
"name": "cefr_check",
|
||||||
|
"value": "\"translations\".\"cefr_level\" IN ('A1', 'A2', 'B1', 'B2', 'C1', 'C2')"
|
||||||
|
},
|
||||||
|
"difficulty_check": {
|
||||||
|
"name": "difficulty_check",
|
||||||
|
"value": "\"translations\".\"difficulty\" IN ('easy', 'intermediate', 'hard')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"verification_identifier_idx": {
|
||||||
|
"name": "verification_identifier_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "identifier",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,13 @@
|
||||||
"when": 1775986238669,
|
"when": 1775986238669,
|
||||||
"tag": "0004_red_annihilus",
|
"tag": "0004_red_annihilus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776154563168,
|
||||||
|
"tag": "0005_broad_mariko_yashida",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"drizzle-kit": "^0.31.10"
|
"drizzle-kit": "^0.31.10"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./dist/src/index.js",
|
||||||
"./schema": "./src/db/schema.ts"
|
"./schema": "./dist/src/db/schema.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,20 +100,6 @@ export const translations = pgTable(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const users = pgTable("users", {
|
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
|
||||||
openauth_sub: text().unique().notNull(),
|
|
||||||
email: varchar({ length: 255 }).unique(),
|
|
||||||
display_name: varchar({ length: 100 }).unique(),
|
|
||||||
created_at: timestamp({ withTimezone: true }).defaultNow().notNull(),
|
|
||||||
last_login_at: timestamp({ withTimezone: true }),
|
|
||||||
});
|
|
||||||
// KNOWN LIMITATION: email is nullable (GitHub users may have no public email)
|
|
||||||
// and unique, but two OAuth providers can return the same email for different
|
|
||||||
// accounts. For MVP this is acceptable since users are identified by
|
|
||||||
// openauth_sub, not email. If multi-provider login per user is added later,
|
|
||||||
// consider a separate user_emails table.
|
|
||||||
|
|
||||||
export const decks = pgTable(
|
export const decks = pgTable(
|
||||||
"decks",
|
"decks",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./dist/src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue